klimax as a Docker host
klimax runs a Docker daemon inside its VM to host kind clusters. That daemon is an ordinary one — you can point your Mac at it and use it for anything, with no Docker Desktop installed.
Point Docker at it
Two ways, and they conflict — pick one. A Docker context persists across shells and is the better default:
$ klimax docker-context # create and switch to the "klimax" context
$ docker ps # now talks to the VM
$ klimax docker-context --unset # switch back to "default"
Or set DOCKER_HOST for the current shell only:
$ eval $(klimax docker-env)
$ eval $(klimax docker-env --unset)
DOCKER_HOST overrides the active
context. If you set both you will eventually confuse yourself about
which daemon you are talking to — use one mechanism.
Either way the socket is a Unix socket at
~/.klimax.docker.sock, forwarded by Lima. Nothing listens on
a TCP port, so nothing on your network can reach the daemon.
Docker Compose
docker compose is a client-side plugin,
not something the daemon provides. The klimax VM ships it, so
klimax shell docker compose version works — but that says
nothing about your Mac, where the docker CLI looks for
plugins in ~/.docker/cli-plugins/. Installing Compose with
Homebrew does not put it there:
$ brew install docker-compose
$ docker compose version
docker: unknown command: docker compose # the binary exists, the plugin link does not
Link it once:
$ mkdir -p ~/.docker/cli-plugins
$ ln -sfn /opt/homebrew/opt/docker-compose/bin/docker-compose \
~/.docker/cli-plugins/docker-compose
$ docker compose version
Homebrew suggests an alternative that picks up future plugins
automatically — add to ~/.docker/config.json:
{
"cliPluginsExtraDirs": ["/opt/homebrew/lib/docker/cli-plugins"]
}
This is not specific to klimax — the same applies to any Docker
daemon you point a Homebrew-installed CLI at. It is worth knowing here
because Docker Desktop bundles its own CLI with the plugins already
wired, so people moving away from it meet this on their first
docker compose up.
Published ports on localhost
This is the part worth configuring deliberately. Set:
network:
disablePortMirroring: false
and a container's published ports appear on your Mac's loopback, exactly as they would with Docker Desktop:
$ docker run -d -p 8080:80 nginx:alpine
$ curl http://localhost:8080/ # 200 — no port-forward, no VM IP
Lima's hostagent watches for listening ports inside the VM and mirrors
each one to 127.0.0.1 on the host as it appears. You publish
a port the way you always have, and it lands where you expect.
Why this is not the default. klimax defaults to
disablePortMirroring: true so it can coexist with other
Lima-based tools that manage kind clusters — Rancher Desktop,
kind-on-lima, Colima. Two VMs both mirroring an API server on port 7001
to 127.0.0.1 break each other. If klimax is your only Lima
VM, or you are using it primarily as a Docker host, turning mirroring on
is the friendlier choice.
The trade-off is small and worth knowing: with mirroring
on, cluster kubeconfigs use 127.0.0.1, which
is stable across VM restarts. With it off, they use the
VM's lima0 IP, which macOS assigns and can change — so
kubeconfigs need refreshing after a restart. Turning mirroring on
actually removes that chore.
This is a VM-level setting: it applies when the instance is created.
Changing it needs klimax destroy && klimax up, which
deletes your clusters. Decide before you build a lab on it.
Bind mounts
The daemon runs inside the VM and resolves
-v sources there. A path your Mac has but the VM does not
will not fail — the guest creates an empty directory and
the container sees nothing in it. Share the directory first:
vm:
mounts:
- location: "~/projects"
writable: true
Each entry appears at the same absolute path inside the VM,
which is what makes docker run -v ~/projects/app:/app
resolve. See Sharing host
directories.
Registry mirrors and the docker CLI
klimax runs pull-through caches for Docker Hub, quay.io, gcr.io and
Artifact Registry, and all of them serve the docker
CLI, not just cluster pulls. A docker pull or
docker compose up is answered from the local cache on the
second fetch, and Docker Hub rate limits stop biting.
Cluster pulls and CLI pulls reach the caches through separate config trees, because two different daemons are involved:
| Consumer | Reads | Written by |
|---|---|---|
docker pull / docker compose | /etc/docker/certs.d/<host>/hosts.toml | klimax up, in the VM |
| kind cluster nodes | /etc/containerd/certs.d/<host>/hosts.toml | klimax cluster create, per node |
Both use containerd's hosts.toml format — dockerd calls
containerd's loader for it — but neither daemon reads the other's
directory. Despite the name, certs.d is a general
per-registry configuration directory: it held only TLS material when
Docker introduced it, and hosts.toml was added to the same
layout later to declare mirrors.
Check what a pull will do:
$ klimax shell sudo ls /etc/docker/certs.d
docker.io
gcr.io
quay.io
us-central1-docker.pkg.dev
us-docker.pkg.dev
This needs Docker's containerd image store, the
default since Docker 28 and what the klimax VM uses. On the older
graphdriver store, dockerd resolves registries with its own client and
can only mirror Docker Hub — so klimax also writes a Hub mirror into
daemon.json, which covers that case.
A mirror is only ever an accelerator: hosts.toml records
the real registry as a fallback, so if a cache is down the pull goes
direct rather than failing.
If you write these files yourself, the endpoint must be
127.0.0.1:<port>, not the mirror's container name.
dockerd runs in the VM's own network namespace, where
registry-dockerio does not resolve — and a mirror dockerd
cannot reach is skipped silently, so pulls keep working while
nothing is ever cached.
What you get over a bare Docker daemon
| Feature | What it means here |
|---|---|
| Pull-through registry mirrors | Docker Hub, quay.io, gcr.io and Artifact Registry are cached locally, for the docker CLI as well as for clusters. Re-pulls are local and Docker Hub rate limits stop biting — see above. |
| Persistent image store | vm.imageDisk keeps images on their own disk, so klimax destroy does not throw away your image cache. |
| Rosetta for amd64 | vm.rosetta: true runs amd64 images on Apple Silicon. |
| Proxy and CA support | network.proxy and vm.caCerts make it work on a corporate network — see Behind a corporate proxy. |
| Kubernetes when you want it | The same daemon hosts kind clusters. A Docker host today, a multi-cluster lab tomorrow, no second VM. |
Alongside Docker Desktop, OrbStack or Colima
They can all be installed at once — each provides its own Docker context, and you switch between them:
$ docker context ls
NAME DESCRIPTION DOCKER ENDPOINT
default Current DOCKER_HOST based ... unix:///var/run/docker.sock
klimax * unix:///Users/you/.klimax.docker.sock
What they cannot all do at once is publish to the same port on
127.0.0.1. If you run klimax with
disablePortMirroring: false next to another tool, give your
containers distinct host ports.
If you want a Docker daemon and nothing else, OrbStack or Colima start faster and are built for that. klimax earns its place when you also want kind clusters with routable LoadBalancer IPs — see klimax vs. other tools.
See also
- Configuration —
disablePortMirroring,vm.mounts, proxy and CA settings - Registry mirrors — what is cached and how to add your own
- klimax vs. other tools