Self-hosted service
tilt-dev/ctlptl avatar
tilt-dev/ctlptl

ctlptl: declaring a local Kubernetes cluster in YAML instead of clicking

Making local Kubernetes clusters fun and easy to set up

734 stars48 forksGoApache-2.0

At a glance

What is it?
ctlptl is a Go CLI that treats a local Kubernetes cluster as a declarative resource, so you describe it in a small YAML file with an apiVersion of ctlptl.dev/v1alpha1 and let the tool create, inspect or delete it across Docker for Desktop, kind, k3d and minikube. It suits developers who want the same cluster definition in a repository, and it is explicitly not a competitor to kind or minikube but a layer that complements them, with a fixed support matrix and no remote cluster story.
Who is it for?
Adopt ctlptl if your team's local cluster setup is currently a sequence of manual steps or a wiki page, and you want that setup committed to the repository as a ctlptl.dev/v1alpha1 Cluster resource that every machine can reproduce. Do not adopt it to manage production or remote clusters, because the tool states that is not what it is for, and check first that the product you use is on the supported list, since microk8s, Rancher Desktop and Podman appear only as future work.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 35 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Four commands, and a YAML shape borrowed from Cluster API

ctlptl, pronounced cattle patrol, is a CLI for declaratively setting up local Kubernetes clusters. The name is a joke about a cow and a cluster, and the tool's whole design is the opposite of the manual approach it replaces.

The inspiration is stated in the README. It is inspired by kubectl and by Cluster API's clusterctl, and the workflow is that you declare your local cluster with YAML and use ctlptl to set it up. The API group is `ctlptl.dev/v1alpha1` and the kind is `Cluster`, which is a deliberate echo of how Cluster API models a cluster as an object with a spec.

There are four major commands:

- `ctlptl get` - see all running clusters - `ctlptl create cluster [product]` - create a cluster and make it the current `kubectl` context - `ctlptl apply -f cluster.yaml` - ensure a cluster exists, or create one - `ctlptl delete -f cluster.yaml` - delete a cluster and its state

The `apply` command is the one that carries the idea. Its semantics are ensure, not create, so the same command is idempotent: run it and the cluster matches the file, run it again and nothing changes. That is the difference between a script that recreates a cluster and a declaration that describes one.

The `get` command is also worth noting, because it is how you discover the name ctlptl assigned. The kind example below uses `kind-kind` as a cluster name, which is the product name followed by the cluster name, and that naming convention is what `ctlptl get` will show you.

The tool is written in Go, licensed under Apache-2.0 with the copyright held by Docker, Inc., and the repository has cmd/, pkg/, internal/, docs/, examples/, hack/ and test/ directories, plus a Makefile, an INSTALL.md, a NOTICE file and a .goreleaser.yml for the release binaries.

Install it with brew, scoop or go install

The installation section asks you to install your cluster of choice first, listing Docker for Desktop, Kind, k3d and Minikube, and then to install ctlptl itself. The order matters: ctlptl drives those tools, so it is not a replacement for any of them.

Three installers are given.

For Mac and Linux, Homebrew:

bash
brew install tilt-dev/tap/ctlptl

For Windows, Scoop, with the tilt-dev bucket added first:

bash
scoop bucket add tilt-dev https://github.com/tilt-dev/scoop-bucket
scoop install ctlptl

And for Go developers, the standard module install:

bash
go install github.com/tilt-dev/ctlptl/cmd/ctlptl@latest

If automatic installers are not what you want, the README points to an installation appendix, INSTALL.md, for more options.

The Makefile in the repository shows the source install path as well, and it is a plain Go install with cgo disabled:

The install target runs `CGO_ENABLED=0 go install ./cmd/ctlptl`.

The `CGO_ENABLED=0` is the detail that matters for distribution: a static binary runs on any machine with the same architecture and operating system, which is what you want from a CLI that gets downloaded on a developer laptop. The rest of the Makefile covers running the tests with a 30 second timeout, a `generated` target that runs `hack/make-rules/generated.sh`, formatting with goimports, `go mod tidy`, an end to end target of `test/e2e.sh` and a golangci-lint target that installs the linter at a pinned version.

The go.mod file requires Go 1.26.0 and pulls in the Kubernetes client libraries at v0.37.0, the kind library at v0.33.0, the Docker CLI and the moby client, cobra for the command line, and `github.com/tilt-dev/localregistry-go` and `github.com/tilt-dev/clusterid` for the registry and cluster naming work, plus `github.com/phayes/freeport` for picking a free port.

Docker for Desktop, where the CPU count is the setting people actually want

The Docker for Desktop examples are the shortest, and they show the pattern that the other products follow.

To enable Kubernetes and set 4 CPUs, you can issue two commands:

bash
ctlptl docker-desktop open
ctlptl create cluster docker-desktop --min-cpus=4

or do the same declaratively, piping a heredoc into `apply`:

bash
cat <<EOF | ctlptl apply -f -
apiVersion: ctlptl.dev/v1alpha1
kind: Cluster
product: docker-desktop
minCPUs: 4
EOF

The `min-cpus` flag on the command line and the `minCPUs` field in the YAML are the same setting in two spellings, which is a slightly awkward consequence of a Go CLI flag and a YAML field having separate names. Worth knowing before you grep for one of them.

Resetting is the same idea in reverse, with a dedicated subcommand for the product and a separate `quit` for the application itself:

bash
ctlptl delete cluster docker-desktop
ctlptl docker-desktop quit

There is also a pair of direct settings commands that bypass the cluster abstraction entirely:

bash
ctlptl docker-desktop set vm.resources.cpus 1
ctlptl docker-desktop set kubernetes.enabled false

That last one, disabling Kubernetes entirely, and the CPU limit of 1, are the two operations a Docker for Desktop user reaches for when a local cluster is eating their laptop's resources. The `set` form takes a dotted key path, which is the same style Docker Desktop's own settings use.

The declaration of the Cluster resource is only three or four lines, which is the point of the design. A ctlptl Cluster file is small enough to sit next to a project's other configuration and to be read by someone who has never used the tool.

Registries at a random port or a fixed one, and how to find the URL

The registry support is the part of ctlptl that goes beyond creating a bare cluster, and it is where the kind and k3d examples get interesting.

With kind, a built-in registry at a random port is a single command:

bash
ctlptl create cluster kind --registry=ctlptl-registry

which in YAML is the same thing with the `registry` field set:

bash
cat <<EOF | ctlptl apply -f -
apiVersion: ctlptl.dev/v1alpha1
kind: Cluster
product: kind
registry: ctlptl-registry
EOF

If you need a known port, the registry is created first as its own object and then referenced by name:

bash
ctlptl create registry ctlptl-registry --port=5005
ctlptl create cluster kind --registry=ctlptl-registry

and the YAML version of that is a two-document file, with a `Registry` kind and a `Cluster` kind separated by the YAML document marker:

bash
cat <<EOF | ctlptl apply -f -
apiVersion: ctlptl.dev/v1alpha1
kind: Registry
name: ctlptl-registry
port: 5005
---
apiVersion: ctlptl.dev/v1alpha1
kind: Cluster
product: kind
registry: ctlptl-registry
EOF

The k3d example is identical apart from the product name, which tells you the Cluster and Registry resources are product agnostic and the product field is what selects the implementation.

With a random port, you need to ask the cluster where the registry ended up, and that is what the `get` command with an output template is for:

bash
ctlptl get cluster kind-kind -o template --template '{{.status.localRegistryHosting.host}}'

The `kind-kind` name is the product followed by the cluster name, and the template path `status.localRegistryHosting.host` is a field on the Cluster's status. This output template mechanism is the kubectl convention, and it is what lets a script capture the registry URL rather than hard coding it.

The Registry resource is not a ctlptl invention. The project lists as a goal to encourage standards that enable interop between devtools, and names KEP 1755, Communicating a Local Registry, as the work that defines how a local registry announces its address to a cluster. The go.mod includes `github.com/tilt-dev/localregistry-go`, which is the client side of that convention.

The same declaration shape across minikube, kind and k3d

The minikube example adds one more field, and it is a useful one to see because it shows the model is product neutral with a product specific extension point.

bash
ctlptl create cluster minikube --registry=ctlptl-registry --kubernetes-version=v1.18.8
bash
cat <<EOF | ctlptl apply -f -
apiVersion: ctlptl.dev/v1alpha1
kind: Cluster
product: minikube
registry: ctlptl-registry
kubernetesVersion: v1.18.8
EOF

A pinned Kubernetes version of v1.18.8 is a fairly old release, and the point of the example is that you can pin it. Whether you would want to is a separate question, but the capability matters for teams that need to reproduce a specific control plane version locally, or that are testing a version skew scenario between a local cluster and a cluster elsewhere.

Looking across the examples, the Cluster resource has a small, stable core: `apiVersion`, `kind`, `product`, plus whichever optional fields apply. The optional fields seen are `minCPUs`, `registry` and `kubernetesVersion`. Anything else, and the examples directory under ./examples is where to look, which contains `docker-desktop.yaml`, `k3d.yaml`, `k3d-config.yaml`, `k3d-registry.yaml`, `kind.yaml`, `kind_custom_config.yaml`, `kind_extra_args.yaml`, `kind_registry_auth.yaml`, `minikube.yaml`, `minikube-k8s-14.yaml` and `registry.yaml`.

Those file names describe the feature surface more precisely than the feature list does. There is a custom config for kind, extra arguments for kind, registry authentication for kind, a k3d config file, and a minikube file pinned to Kubernetes 1.4.

The API reference is published on pkg.go.dev under pkg/api, with the Cluster type, and the complete CLI documentation is in docs/ctlptl.md in the repository. The `get` command with an output template is the escape hatch for anything not in the reference, and it follows the kubectl convention closely enough that a kubectl user will not need to learn a new query language.

What ctlptl explicitly is not, and the product list it does not cover yet

The non-goals section is the most useful part of the README for deciding whether the tool fits, because it is unusually explicit.

ctlptl is NOT a Kubernetes setup approach that competes with kind or minikube, but rather complements these tools. And it is NOT intended to help you set up a remote cluster, or a remote dev sandbox; if you want to declaratively set up prod clusters, the README points at clusterapi.

Those two sentences rule out the two wrong readings of the project. It is not trying to be a better kind, and it is not a way to manage real infrastructure.

The stated goals are to support common local cluster setup operations like create, delete and reset, to interoperate well with all local Kubernetes solutions including docker-desktop, kind, minikube, k3d or microk8s, to connect other resources to a local cluster such as image registries, storage and CPU and memory, to help infra engineers manage a consistent dev environment, and to encourage standards that enable interop between devtools.

The current feature list is shorter than the goal list, and the gap between them is where the limitations are. Current support covers Docker for Mac, Docker for Windows, kind and kind with a registry, minikube and minikube with a registry, k3d with a registry, creating a cluster on a remote Docker host, which is called out as useful in CI environments such as CircleCI, and allocating CPUs.

The future work list names microk8s, Rancher Desktop, Podman, Minikube on Hyperkit, allocating memory and allocating storage. So memory and storage allocation, which the goals mention, are not there yet, and the project list is narrower than the aspiration list: if you use microk8s or Podman, ctlptl cannot drive them today.

The one that is worth planning around is the remote Docker host support, since it is the feature that makes ctlptl usable in CI, where there is no Docker Desktop to click on. The repository carries a `.circleci/` directory and a `hack/publish-ci-image.sh` script with a Makefile target for it, so the CI image is published rather than built locally.

On privacy, the tool sends anonymised usage statistics so it can be improved on every platform, and you can opt out with `ctlptl analytics opt out`.

Editorial conclusion

Adopt ctlptl if your team's local cluster setup is currently a sequence of manual steps or a wiki page, and you want that setup committed to the repository as a ctlptl.dev/v1alpha1 Cluster resource that every machine can reproduce. Do not adopt it to manage production or remote clusters, because the tool states that is not what it is for, and check first that the product you use is on the supported list, since microk8s, Rancher Desktop and Podman appear only as future work.

Frequently asked questions

What does ctlptl do?

It is a CLI for declaratively setting up local Kubernetes clusters, inspired by kubectl and Cluster API's clusterctl. You declare a cluster in YAML with apiVersion ctlptl.dev/v1alpha1 and kind Cluster, and ctlptl sets it up across Docker for Desktop, kind, k3d and minikube.

How do I install ctlptl?

With `brew install tilt-dev/tap/ctlptl` on Mac and Linux, with `scoop bucket add tilt-dev https://github.com/tilt-dev/scoop-bucket` followed by `scoop install ctlptl` on Windows, or with `go install github.com/tilt-dev/ctlptl/cmd/ctlptl@latest`. An INSTALL.md appendix has further options.

What is the difference between ctlptl create cluster and ctlptl apply?

create cluster makes a cluster and sets it as the current kubectl context, while apply with a file ensures a cluster exists or creates one, so it is idempotent and the same command can be run repeatedly against a committed YAML file. delete with the same file removes a cluster and its state.

How do I find the registry URL when ctlptl picks a random port?

Query the cluster status with `ctlptl get cluster kind-kind -o template --template '{{.status.localRegistryHosting.host}}'`, which reads the localRegistryHosting host field. If you want a fixed port, create the registry first with `ctlptl create registry ctlptl-registry --port=5005` and reference it from the Cluster.

Can ctlptl manage a production or remote cluster?

No. The README lists as a non-goal setting up a remote cluster or a remote dev sandbox, and points at clusterapi for declaratively setting up prod clusters. What it does support is creating a cluster on a remote Docker host, which is useful in CI environments like CircleCI.

Is ctlptl still being maintained?

The repository is not archived and the last push was on 2026-09-18. Recent releases include v0.9.6 from 2026-09-10 and v0.9.5 from 2026-08-27, and the code is Apache-2.0 licensed with the copyright held by Docker, Inc.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. README
  4. Releases
  5. tilt-dev/ctlptl on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/tilt-dev-ctlptl.svg)](https://hysenlabs.com/projects/tilt-dev-ctlptl)

Was this useful?