kube-coder normally runs on a managed Kubernetes cluster (the reference deploy is DigitalOcean DOKS), but it runs just as well on a local single-node cluster for development, evaluation, or offline use — no cloud account, no container registry, no public DNS, and no TLS certificates required.
This guide uses minikube, because its
built-in ingress addon ships the same ingress-nginx controller the chart
expects, and minikube image load lets you run a locally-built image without
pushing it anywhere. kind and
k3d work too — see Other local clusters.
The whole flow is wrapped in make local-* targets that talk only to the
minikube context, so they can't touch a remote cluster.
| Tool | Why | Install (macOS) |
|---|---|---|
| Docker | minikube's driver + image build | Docker Desktop |
| minikube | the local cluster | brew install minikube |
| kubectl | talk to the cluster | brew install kubectl |
| helm | install the charts | brew install helm |
Give Docker Desktop enough headroom (Settings → Resources): the make local
target starts minikube with --cpus=4 --memory=6g, so allocate at least that
to Docker. The workspace image is large (~2–3 GB) because it bundles
code-server, Claude Code, OpenCode, Ante, LibreFang, ttyd, and a full toolchain.
Apple Silicon (M-series): the build targets your host architecture automatically (
linux/arm64), so it runs natively with no emulation. Every bundled binary (code-server, ttyd, LibreFang, …) has an arm64 build, so this is a first-class path, not a fallback.
# One command: start the cluster, build + load the image, deploy, print access info.
make localThat runs, in order: local-up → local-build
→ local-secret → local-deploy
→ local-info. The first run takes a while (image build + load); later runs are cached.
When it finishes, local-info prints the three steps to reach the dashboard:
# 1. Map the hostname to localhost (one time, needs sudo):
echo '127.0.0.1 kube-coder.local' | sudo tee -a /etc/hosts
# 2. Forward the ingress controller (keep this running in its own terminal):
make local-forward
# 3. Open the dashboard and log in with basic auth admin / admin:
open http://kube-coder.local:8080/That's it — you're in the kube-coder dashboard running entirely on your laptop.
Why port-forward instead of the minikube IP? With the Docker driver on macOS and Windows, the minikube node IP isn't routable from the host. A
kubectl port-forwardto the ingress controller works identically on every OS, and routing still goes through the ingress (so host-based routing and basic auth behave exactly like production). On Linux you can instead point/etc/hostsat$(minikube ip -p kube-coder)and skip the port-forward.
You can run the steps individually (e.g. to rebuild after a code change).
Starts the minikube profile kube-coder with the Docker driver and enables the
ingress addon (installs ingress-nginx), then waits for the controller pod.
Builds the workspace image for your host arch (docker buildx ... --load) as
kube-coder:local, then minikube image loads it into the cluster. No registry
involved. Re-run this after editing the Dockerfile or anything baked into the
image (the dashboard SPA, installed CLIs).
Creates the coder namespace and a kube-coder-basic-auth Secret containing an
htpasswd entry for admin / admin (generated with openssl passwd -apr1).
The ingress references this Secret for HTTP basic auth. Change the credentials
by overriding LOCAL_AUTH_USER / LOCAL_AUTH_PASS:
make local-secret LOCAL_AUTH_USER=me LOCAL_AUTH_PASS=hunter2Installs the base-infrastructure chart (the kaniko-wrapper ConfigMap the
workspace pod mounts) and then the workspace chart using
deployments/local/values.yaml, and waits
for the ws-local rollout.
The local overlay lives at deployments/local/values.yaml
and is safe to commit (no secrets). The local-specific choices:
| Setting | Local value | vs. cloud default |
|---|---|---|
image.repository / tag |
kube-coder:local |
DO registry image |
image.pullPolicy |
IfNotPresent |
Always |
image.pullSecretName |
empty | regcred |
ingress.tls.enabled |
false |
true (cert-manager + Let's Encrypt) |
ingress.auth.type |
basic |
oauth2 / basic |
user.host |
kube-coder.local |
a real DNS name |
user.pvcSize |
10Gi |
50Gi |
resources |
smaller | full |
The workspace boots without any AI keys — you can log into Claude Code
interactively by running claude once in the pod terminal. To bake keys in,
drop a gitignored overlay at secrets/local/keys.yaml:
claude:
apiKey: "sk-ant-..." # optional; or log in interactively instead
assistant:
openrouter:
apiKey: "sk-or-..."…and add -f secrets/local/keys.yaml to the local-deploy helm command (or
re-run helm upgrade yourself). See environment-variables.md
and llm-setup.md.
make local-info # reprint access details
make local-forward # (re)start the ingress port-forward
make local-build && make local-deploy # rebuild image + redeploy after changes
# Direct cluster access (note: --context kube-coder, never your remote cluster)
kubectl --context kube-coder -n coder get pods
kubectl --context kube-coder -n coder logs -f deploy/ws-local -c ide
kubectl --context kube-coder -n coder exec -it deploy/ws-local -c ide -- bashmake local-down # remove the kube-coder workspace, keep the cluster
make local-down DELETE=1 # also delete the minikube profile entirely- In-workspace image builds are disabled. The
docker-buildhelper uses Kaniko, which needs a registry to push to; the local overlay leaves the push target empty. kube-coder itself runs fine — you just can't build and push container images from inside a workspace. To enable it, runminikube addons enable registryand pointbuild.pushSecretName/build.defaultDestinationRepoat it. - Single node, single workspace. The local overlay provisions one workspace
(
local). The cloud per-user flow (make new-user,make deploy USER=…) is for multi-tenant clusters. - No TLS. Local mode serves plain HTTP. That's fine on localhost; don't expose it.
- Resources. Defaults are laptop-sized; heavy builds/tests inside the
workspace may want a bigger
minikube start --memory. - Apple Silicon: the in-workspace browser/Desktop is degraded. The image
bundles Firefox via Mozilla's
linux64(x86_64) build, which can't execute on anarm64node. The image still builds, and the dashboard, terminal, tasks, Memory, Files, code-server, and Claude all work — but the noVNC Desktop tab (which launches Firefox/X11) won't render. The dashboard opens on the Desktop tab by default, so just click Build (Tasks) or another tab. Onamd64hosts the Desktop tab works normally. - Auth model. Local uses http basic auth, where the ingress is the sole
authenticator and server.py trusts requests that reach it (ingress-nginx
strips the credential header and blocks re-forwarding it, so the backend
can't re-verify). This is fine for a single-tenant local cluster; for
multi-tenant clusters use
ingress.auth.type=oauth2, where server.py enforces identity itself.
| Symptom | Fix |
|---|---|
ErrImageNeverPull / ImagePullBackOff on ws-local |
The image wasn't loaded. Re-run make local-build (it both builds and minikube image loads). |
http://kube-coder.local:8080 won't resolve |
Add the /etc/hosts line (127.0.0.1 kube-coder.local) and make sure make local-forward is running. |
| 401 / auth prompt loops | Credentials are admin / admin (or whatever you set via LOCAL_AUTH_*). Re-run make local-secret if you changed them. |
Pod stuck ContainerCreating |
Usually the PVC or the kaniko-wrapper ConfigMap — confirm make local-deploy installed base-infrastructure. |
| minikube won't start / OOMs | Raise Docker Desktop's memory, or lower it: make local-up then minikube start -p kube-coder --memory=4g. |
The chart is cluster-agnostic; only the make local-* wrappers assume minikube.
To use a different tool, replicate the four steps against it:
- kind: create the cluster, install
ingress-nginx(kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml), load the image withkind load docker-image kube-coder:local, thenhelm upgrade --installwithdeployments/local/values.yaml. - k3d:
k3d cluster create --k3s-arg "--disable=traefik@server:0", installingress-nginx, push to the built-ink3dregistry (or import the image), then deploy the same overlay.
The deployments/local/values.yaml overlay works unchanged on any of them.