Docker & Containers
Run ZeroClaw in Docker, Podman, Kubernetes, or any OCI runtime.
Official images
Pushed to GitHub Container Registry (ghcr.io) on every stable release:
ghcr.io/zeroclaw-labs/zeroclaw:latest: latest stableghcr.io/zeroclaw-labs/zeroclaw:v0.7.5: pinnedghcr.io/zeroclaw-labs/zeroclaw:debian: Debian-based image (larger, broader glibc support)
Multi-arch: linux/amd64, linux/arm64.
Note on shell access: The default
latestimage is intentionally distroless and does not includesh,ash, orbash. Use thedebiantag if you need a shell inside the container (for example, to rundocker execfor debugging).
Minimum run
sh
docker run -d \
--name zeroclaw \
-v zeroclaw-data:/zeroclaw-data \
-p 42617:42617 \
ghcr.io/zeroclaw-labs/zeroclaw:latest
The official image already binds [::] with allow_public_bind = true and require_pairing = false baked into its default config, so the published port is reachable out of the box. The ZEROCLAW_gateway__allow_public_bind override below only matters if you bind-mount your own config (which replaces the baked one) that defaults to localhost.
The image expects persistent state at /zeroclaw-data. On first run, it bootstraps a default config: you still need to run quickstart before it’s useful:
sh
docker exec -it zeroclaw zeroclaw quickstart
Running zerocode (the TUI)
The image ships the zerocode terminal interface alongside the zeroclaw binary. The default entrypoint is zeroclaw, so launch zerocode by overriding it with --entrypoint zerocode and an interactive TTY (-it). Both image variants carry it:
distroless (:latest)
docker run -it --entrypoint zerocode ghcr.io/zeroclaw-labs/zeroclaw:latest
debian
docker run -it --entrypoint zerocode ghcr.io/zeroclaw-labs/zeroclaw:debian
zerocode connects to a running ZeroClaw daemon, so point it at one:
- Same container’s daemon: run it against the container that already runs the daemon (
docker exec -it zeroclaw zerocode), which reaches the daemon over the local IPC socket. - A remote daemon: connect over WebSocket Secure with
zerocode --connect wss://<host>:<port>; see Remote setup (WSS). This is the portable way to drive a containerized or remote daemon from your own terminal.
Persist /zeroclaw-data (as in Minimum run) so the config and identity zerocode reads are the same ones the daemon uses.
Compose
A minimal docker-compose.yml:
services:
zeroclaw:
image: ghcr.io/zeroclaw-labs/zeroclaw:latest
restart: unless-stopped
ports:
- "42617:42617" # gateway
volumes:
- ./data:/zeroclaw-data
# The official image already enables public bind; only add an `environment:`
# block with the override above if you bind-mount a localhost-default config.
After the container starts, run quickstart:
sh
docker compose exec zeroclaw zeroclaw quickstart
With the official image you can omit the ZEROCLAW_gateway__allow_public_bind override entirely; it is already enabled in the baked config.
macOS: OrbStack vs Colima
macOS has no native Linux kernel, so every option (Docker Desktop, Podman, OrbStack, Colima) runs the container inside a lightweight Linux VM. For a Mac dev box, the two mac-native VMs worth comparing are OrbStack and Colima, both run the container with the same docker run/Compose commands above.
| OrbStack | Colima | |
|---|---|---|
| Engine | custom, tuned Linux VM (Apple Silicon optimized) | Lima VM + containerd/Docker |
| License | commercial, freemium (free personal use) | MIT (Lima underneath is Apache 2.0) |
| Interface | GUI app + CLI | CLI-first (colima start/stop), scriptable |
| Best when | minimal fuss, polished UX | everything OSS, config in code |
OrbStack
# Provides the docker CLI:
brew install --cask orbstack
Colima
# docker CLI talks to colima's VM:
brew install colima docker docker-compose # docker-compose = the Compose v2 plugin; install if you need `docker compose`
colima start --cpu 4 --memory 8 # add --network-address to expose the VM IP to macOS
Performance is comparable for typical dev workloads; the real differentiators are licensing (commercial vs OSS) and UX preference, not raw speed; benchmark both on your own machine if idle RAM or build throughput matters. Either way you drive the engine inside the VM with docker; systemd quadlets (below) are a Linux-host feature and don’t apply on macOS.
Podman & systemd quadlets
On a Linux server, the cleanest way to run the container long-term is a Podman quadlet: a declarative unit file that systemd turns into a real service. You get systemctl lifecycle, journald logs, auto-restart, and boot ordering with no daemon and no --restart hack, and the unit file is config you commit to git. This is the recommended server pattern; docker run/Compose are fine for a laptop.
A quadlet is a *.container file (siblings: .pod, .volume, .network, .kube, .build, .image). Podman’s systemd generator reads it on every daemon-reload and writes a transient .service; you never author the .service yourself.
Rootful units live in /etc/containers/systemd/; rootless in ~/.config/containers/systemd/.
/etc/containers/systemd/zeroclaw.container:
[Unit]
Description=ZeroClaw agent runtime
After=network-online.target
Wants=network-online.target
[Container]
# Pin a release in production; :latest is distroless (no shell — use :debian to exec a shell).
Image=ghcr.io/zeroclaw-labs/zeroclaw:latest
ContainerName=zeroclaw
PublishPort=42617:42617
Volume=zeroclaw-data:/zeroclaw-data
# The official image already binds publicly; add an `Environment=` line with the
# allow-public-bind override only if you mount a localhost-default config.
# Optional rolling-upgrade path — re-pull a newer image on (re)start and opt into `podman auto-update`:
Pull=newer
AutoUpdate=registry
[Service]
Restart=always
[Install]
WantedBy=multi-user.target default.target
Deploy (idempotent, safe to re-run; re-applying converges the running container, never duplicates it):
sh
sudo cp zeroclaw.container /etc/containers/systemd/
sudo systemctl daemon-reload # generator turns .container into zeroclaw.service
sudo systemctl restart zeroclaw
Then onboard once, and manage it like any service:
sh
sudo podman exec -it zeroclaw zeroclaw quickstart
systemctl status zeroclaw
journalctl -u zeroclaw -f
There is no systemctl enable step for generated units: the [Install] WantedBy= line is what brings it up on boot.
- Version pinning vs
:latest. Pin a tag or digest (Image=ghcr.io/zeroclaw-labs/zeroclaw:v0.7.5or...@sha256:...) for reproducible, auditable deploys; upgrading is then a reviewable tag bump in the committed.containerfile.Pull=newer+AutoUpdate=registryinstead give rolling upgrades, driven bypodman-auto-update.timer(sudo systemctl enable --now podman-auto-update.timer). Pick reproducibility or currency; the deploy loop is the same either way. - Rootless variant. Drop the file in
~/.config/containers/systemd/, usesystemctl --user daemon-reload && systemctl --user restart zeroclaw, and runloginctl enable-linger $USERso it survives logout (same lingering note as Service & daemon). - WSL2. Modern WSL2 runs systemd (
[boot] systemd=truein/etc/wsl.conf, thenwsl --shutdown), so this exact quadlet pattern works inside a WSL distro: no Windows-specific dialect.
Config inside containers
The image expects config under /zeroclaw-data/.zeroclaw/. Mount your local config in:
sh
docker run -d --name zeroclaw \
-v $(pwd)/my-config.toml:/zeroclaw-data/.zeroclaw/config.toml:ro \
-v zeroclaw-state:/zeroclaw-data/workspace \
-p 42617:42617 \
ghcr.io/zeroclaw-labs/zeroclaw:latest
For container workloads, set uri on each providers.models.<type>.<alias> to a container-reachable address (e.g. http://host.docker.internal:11434 for an Ollama server on the Docker Desktop host). The generic env-override mechanism can set the same field at runtime without editing the config:
sh
ZEROCLAW_providers__models__ollama__home__uri=http://ollama:11434 zeroclaw agent -a assistant
See Providers → Container-friendly overrides for the grammar.
Channels that poll (Telegram, email): just work
Outbound-initiated channels don’t need any special container configuration. Telegram polling, IMAP, MQTT, Nostr relays: all pull; the container only needs egress.
Channels that receive webhooks: need ingress
Discord, Slack, GitHub, and most webhook channels need inbound HTTP. Two options:
- Expose the gateway:
-p 42617:42617+ reverse proxy with TLS in front, point the webhook URL at the public address - Use a tunnel: ngrok, Cloudflare Tunnel, or Tailscale Funnel; set the tunnel URL as the webhook target
Configure a tunnel by setting the top-level [tunnel] tunnel_provider (override env var: ZEROCLAW_tunnel__tunnel_provider) to one of the supported providers and filling the matching tunnel.* block; the full provider list and per-provider fields are in the Config reference. The resulting public URL is what you point your webhook senders at.
Kubernetes
Helm chart templates are published to the zeroclaw-templates repo. Typical manifest fragment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: zeroclaw
spec:
replicas: 1
strategy:
type: Recreate # ZeroClaw is single-instance per workspace
template:
spec:
containers:
- name: zeroclaw
image: ghcr.io/zeroclaw-labs/zeroclaw:v0.7.5
ports:
- containerPort: 42617
volumeMounts:
- name: data
mountPath: /zeroclaw-data
# The official image already binds publicly; add an `env:` entry for the
# allow-public-bind override only if you mount a localhost-default config.
volumes:
- name: data
persistentVolumeClaim:
claimName: zeroclaw-data
Scaling: ZeroClaw is single-writer per workspace. Don’t scale horizontally; run one instance per agent.
Re-authenticating after logout
If you log out of the web UI while running in a container, the existing paircode becomes invalid. Generate a new one to log back in:
sh
docker exec -it zeroclaw zeroclaw gateway get-paircode --new
For Compose deployments, use docker compose exec instead:
sh
docker compose exec zeroclaw zeroclaw gateway get-paircode --new
Gotchas
- macOS hostname quirks (Docker Desktop, colima, Rancher Desktop).
host.docker.internalworks out of the box on Docker Desktop for macOS. On colima, it is only reachable if you installed withcolima start --network-address(otherwise the container can’t see the host at all; connect via the VM’s gateway IP, usually192.168.5.2, or tunnel through a shared network). Rancher Desktop behaves like Docker Desktop for recent versions but has hadhost.docker.internalresolve-failures on older releases. If provider calls fail withconnection refusedtohost.docker.internal, verify withdocker run --rm alpine getent hosts host.docker.internal: empty output means the hostname isn’t resolvable and you need an explicit IP. - Host-side services. If a provider is Ollama on the host,
uri = "http://host.docker.internal:11434"(under[providers.models.ollama.<alias>]) works on Docker Desktop. On Linux Docker you may need--add-host=host.docker.internal:host-gateway. - Memory persistence. Agent memory (the SQLite
brain.db) lives under the config directory at/zeroclaw-data/.zeroclaw/agents/<alias>/workspace/memory/, with shared instance databases under/zeroclaw-data/data/. Mounting/zeroclaw-datapersists all of it; skip the volume and every restart loses conversation history. - Bind-mounting
/zeroclaw-data. A host bind mount on/zeroclaw-datareplaces the entire image directory, including the default config and (previously) the dashboard bundle. The dashboard is now installed at/usr/share/zeroclawlabs/web/dist, outside the mount, so a bind mount no longer hides it. On first run, mount an empty host directory and the container bootstraps a fresh config; the gateway auto-detects the dashboard from its image path. - No hardware passthrough by default. GPIO / USB need explicit
--deviceflags (--device /dev/ttyUSB0), and the container user needs matching GID fordialout/gpiogroups.
Next
- Service management
- Operations → Network deployment: tunnels, reverse proxies