Docker engine in a VM (fluxctl docker)
FluxVM already answers a Docker Engine API subset on {run_dir}/docker.sock (see
oci-sandboxes.md): each container is its own microVM, which is fast and isolated, but
the subset cannot build, run Compose, bind-mount Mac paths or create Docker networks.
fluxctl docker is the other half: a real dockerd in one managed FluxVM VM, with its socket on your Mac. Everything the Docker
CLI does works, because it is the real engine: docker build, docker compose up --build --wait, bind mounts, user-defined
networks with name resolution, tmpfs, read_only, cap_drop, health checks.
Built-in docker.sock | fluxctl docker | |
|---|---|---|
| Containers | one microVM each | ordinary containers in one VM |
docker build, Compose | no | yes |
| Bind mounts of Mac paths | no | yes, for folders you share (below) |
| Docker networks | no | yes |
| Start-up | about a second per container | one VM, a few minutes the first time (it installs Docker) |
| Isolation | strongest (VM per container) | the containers share one VM |
Use it
fluxctl docker up --share ~/work # create (first time) or start the VM and publish the socket
eval "$(fluxctl docker env)" # export DOCKER_HOST=unix:///tmp/fluxvm-run-501/docker-engine.sock
docker run --rm alpine:3.22 echo hello
docker build -t app .
docker compose up --build --wait -d
fluxctl docker status # VM, address, whether the socket answers (JSON)
fluxctl docker down # stop the forward and the VM (Docker data kept)
fluxctl docker down --purge # delete the VM and everything Docker stored in it
up is idempotent: run it again to restart the VM or the socket forward. It prints the export DOCKER_HOST=… line.
Bind mounts
--share HOST[:GUEST][:ro] shares a Mac folder into the VM with virtiofs. The guest path defaults to the host path, so a
command run on the Mac such as docker run -v "$PWD/conf:/etc/conf" … or a Compose volumes: ["./conf:/etc/conf"] names a path
that exists, with the same content, inside the VM where dockerd runs. A path outside a shared folder is empty in the guest.
Shares are fixed when the VM is created; to add one, down --purge and up again (or use another --name).
Reach a container's port from the Mac
A container published as -p 127.0.0.1:8080:80 listens on the VM's loopback. Add --publish 8080 (or HOST:GUEST) to up and
the Mac's 127.0.0.1:8080 reaches it over the engine's SSH connection:
fluxctl docker up --publish 8080
docker run -d -p 127.0.0.1:8080:80 nginx && curl -s http://127.0.0.1:8080/ | head -1
Keep it running (LaunchAgent)
fluxctl docker agent install --share ~/work # runs `fluxctl docker supervise` at login, restarts it if it exits
fluxctl docker agent uninstall
supervise checks every 30 seconds (--every) that the VM is running and the socket answers, and repairs either. It never
deletes anything. The agent logs to ~/Library/Logs/fluxvm-docker-engine.log.
How it works
upfinds the VM nameddocker-engine(--name) or creates it: Debian 13,docker.io,docker-composeanddocker-buildxfrom apt (cloud-init), your SSH key, 2 vCPUs, 3 GiB, 40 GiB disk (all flags).- It waits for the guest's address, for SSH, and for
docker infoto succeed. - It starts a detached
ssh -N -L {socket}:/var/run/docker.sockto the guest (with keepalives, plus a-Lper--publish) and records its pid and settings next to the socket. The socket is{run_dir}/docker-engine.sock, beside the built-indocker.sock;--socketoverrides it.
Nothing listens on a TCP port, and the guest's Docker socket is only reachable by your user through the SSH key.
If the first boot stalls
The first boot installs packages with apt, so it needs this Mac to be online. If the VM never reports an address, or Docker is not ready in
15 minutes, the command stops and says so; for a guest that is up it includes what cloud-init says in the guest (cloud-init status and the end of
/var/log/cloud-init-output.log). fluxctl serial NAME shows the guest's console. Fix the network and run the same command again: it picks the VM
up where it was.
In Studio
The Docker page (Platform, in the node Studio) shows the engine the daemon can reach at /tmp/fluxvm-run-<uid>/docker-engine.sock, the socket
fluxctl docker up publishes for the user the daemon runs as: its version, the containers (state, status as Docker words it, published ports,
age, with a filter and a running or not-running switch) and the images (tags, size, containers using them), and a container's log in the same
colourful, filterable window as a pod's. Each container has Start, Stop and Restart buttons (a stop waits for the container's grace period, 10 s by default, then kills it; a toast says what happened, including "already stopped"). Nothing else is written to the engine: no create, remove or pull, which stay with the docker CLI. It is admin only: the containers belong to the whole host. When there is no engine VM
or its socket does not answer, the page says so and names the command to run instead of failing. The routes are GET /v1/docker and
GET /v1/docker/containers/{id}/logs?tail=N and POST /v1/docker/containers/{id}/{start|stop|restart} (in the daemon's OpenAPI document; the request log records each action). A daemon that was given a different --socket for fluxctl docker does not see
that engine.
Limits
- One engine VM per name; it uses its vCPUs and memory while it runs.
- The containers inside share the VM's kernel and resources. For strong per-container isolation use the built-in container sandboxes.
- Only the folders you
--shareexist in the guest. Docker volumes live on the VM's disk and go away withdown --purge. - Published container ports are on the guest's loopback, not the Mac's. Forward the ones you need with
--publish HOST[:GUEST](repeatable; Mac side is always 127.0.0.1), for examplefluxctl docker up --publish 18443 --publish 18081. Changing--publishrestarts the forward without touching the VM. - macOS with Apple silicon only (it uses a
vzVM).
Live check: scripts/docker-engine-live-test.sh.
Need Kubernetes instead? fluxctl kube up does the same for a single-node k3s cluster: kubernetes-on-mac.md.