Skip to main content

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.sockfluxctl docker
Containersone microVM eachordinary containers in one VM
docker build, Composenoyes
Bind mounts of Mac pathsnoyes, for folders you share (below)
Docker networksnoyes
Start-upabout a second per containerone VM, a few minutes the first time (it installs Docker)
Isolationstrongest (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​

  1. up finds the VM named docker-engine (--name) or creates it: Debian 13, docker.io, docker-compose and docker-buildx from apt (cloud-init), your SSH key, 2 vCPUs, 3 GiB, 40 GiB disk (all flags).
  2. It waits for the guest's address, for SSH, and for docker info to succeed.
  3. It starts a detached ssh -N -L {socket}:/var/run/docker.sock to the guest (with keepalives, plus a -L per --publish) and records its pid and settings next to the socket. The socket is {run_dir}/docker-engine.sock, beside the built-in docker.sock; --socket overrides 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 --share exist in the guest. Docker volumes live on the VM's disk and go away with down --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 example fluxctl docker up --publish 18443 --publish 18081. Changing --publish restarts the forward without touching the VM.
  • macOS with Apple silicon only (it uses a vz VM).

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.