-
Notifications
You must be signed in to change notification settings - Fork 4.4k
13.1 Docker Deployment
Relevant source files
The following files were used as context for generating this wiki page:
This document explains the Docker deployment architecture for ZeroClaw, including multi-stage build configuration, image variants, CI/CD publishing workflow, and deployment using Docker Compose. For native binary deployment, see Native Binary Deployment. For production configuration best practices, see Production Configuration.
This page covers:
- Multi-stage Dockerfile structure and build optimization
- Development (
dev) vs. production (release) image variants - GitHub Container Registry (GHCR) publishing workflow
- Docker Compose deployment patterns
- Environment variable configuration and volume management
This documentation is specific to containerized deployments. For information about the security model that applies to both Docker and native deployments, see Security Model.
The ZeroClaw Dockerfile implements a three-stage build process optimized for caching and minimal image size:
graph TB
subgraph "Stage 1: builder"
A1["rust:1.93-slim base image"]
A2["Install pkg-config"]
A3["Copy Cargo.toml, Cargo.lock, rust-toolchain.toml"]
A4["Create dummy source files"]
A5["cargo build --release (dependencies only)"]
A6["Remove dummy files"]
A7["Copy actual source: src/, benches/, crates/, firmware/"]
A8["cargo build --release (application)"]
A9["Strip binary: /app/zeroclaw"]
A10["Create /zeroclaw-data structure"]
A1 --> A2 --> A3 --> A4 --> A5 --> A6 --> A7 --> A8 --> A9 --> A10
end
subgraph "Stage 2: dev"
B1["debian:trixie-slim base"]
B2["Install ca-certificates, curl"]
B3["COPY --from=builder zeroclaw binary"]
B4["COPY --from=builder /zeroclaw-data"]
B5["Overwrite with dev/config.template.toml"]
B6["Set ENV: PROVIDER=ollama, MODEL=llama3.2"]
B7["USER 65534:65534 (nonroot)"]
B1 --> B2 --> B3 --> B4 --> B5 --> B6 --> B7
end
subgraph "Stage 3: release"
C1["gcr.io/distroless/cc-debian13:nonroot"]
C2["COPY --from=builder zeroclaw binary"]
C3["COPY --from=builder /zeroclaw-data"]
C4["Set ENV: PROVIDER=openrouter"]
C5["USER 65534:65534 (nonroot)"]
C1 --> C2 --> C3 --> C4 --> C5
end
A10 -.dev target.-> B3
A10 -.release target (default).-> C2
Sources: Dockerfile:1-113
The builder stage compiles the Rust binary using aggressive caching strategies:
| Build Step | Purpose | Cache Strategy |
|---|---|---|
| Dependency compilation | Build dependencies separately from application code | Mount zeroclaw-cargo-registry and zeroclaw-target caches |
| Dummy source files | Allow cargo build to succeed with manifests only |
Create placeholder main.rs, lib.rs
|
| Application compilation | Build actual binary after dependencies cached | Reuse dependency artifacts from previous step |
| Binary stripping | Reduce binary size by removing debug symbols | Run strip /app/zeroclaw
|
The dependency caching technique is implemented in Dockerfile:19-27:
RUN mkdir -p src benches crates/robot-kit/src \
&& echo "fn main() {}" > src/main.rs \
&& echo "fn main() {}" > benches/agent_benchmarks.rs \
&& echo "pub fn placeholder() {}" > crates/robot-kit/src/lib.rs
RUN --mount=type=cache,id=zeroclaw-cargo-registry,target=/usr/local/cargo/registry,sharing=locked \
--mount=type=cache,id=zeroclaw-cargo-git,target=/usr/local/cargo/git,sharing=locked \
--mount=type=cache,id=zeroclaw-target,target=/app/target,sharing=locked \
cargo build --release --locked
This allows dependencies to be cached even when application source files change, significantly speeding up rebuild times.
Default configuration generation happens inline in the builder stage at Dockerfile:42-56, creating a minimal config.toml with workspace paths pre-configured:
workspace_dir = "/zeroclaw-data/workspace"
config_path = "/zeroclaw-data/.zeroclaw/config.toml"
api_key = ""
default_provider = "openrouter"
default_model = "anthropic/claude-sonnet-4-20250514"
Sources: Dockerfile:4-56
The dev target uses Debian Trixie Slim as the base image, providing a full userland for debugging and development:
| Feature | Configuration | File Reference |
|---|---|---|
| Base image | debian:trixie-slim |
Dockerfile:59 |
| Runtime dependencies |
ca-certificates, curl
|
Dockerfile:62-65 |
| Configuration template | Overwrites minimal config with Ollama defaults | Dockerfile:71 |
| Default provider | PROVIDER=ollama |
Dockerfile:79 |
| Default model | ZEROCLAW_MODEL=llama3.2 |
Dockerfile:80 |
| User |
nonroot (UID/GID 65534) |
Dockerfile:87 |
The development configuration is sourced from dev/config.template.toml, which pre-configures local Ollama usage without requiring external API keys. This allows developers to run docker compose up and immediately interact with a local LLM.
Sources: Dockerfile:58-91
The release target (default) uses Google's Distroless CC image for minimal attack surface:
| Feature | Configuration | File Reference |
|---|---|---|
| Base image | gcr.io/distroless/cc-debian13:nonroot |
Dockerfile:93 |
| Image size | Minimal (no shell, package manager, or utilities) | N/A |
| Default provider | PROVIDER=openrouter |
Dockerfile:103 |
| API key requirement | Must be provided via API_KEY environment variable |
Dockerfile:106 |
| User |
nonroot (UID/GID 65534) |
Dockerfile:109 |
The distroless base eliminates unnecessary binaries, reducing the image size and CVE surface area. Note that the absence of a shell means debugging must be done via external tools or by temporarily switching to the dev target.
Sources: Dockerfile:92-113
docker build -t zeroclaw:latest .This builds the release stage by default (the last stage in the Dockerfile).
docker build --target dev -t zeroclaw:dev .The --target dev flag stops the build at the dev stage, creating a Debian-based image suitable for local development.
The Dockerfile uses BuildKit mount caches to persist Cargo registry, git repositories, and build artifacts across builds. To verify cache usage:
DOCKER_BUILDKIT=1 docker build --progress=plain -t zeroclaw:latest .Look for cache mount hits in the build output (lines showing [cached] or CACHED).
Sources: Dockerfile:23-26, Dockerfile:34-39
ZeroClaw images are automatically published to GitHub Container Registry (GHCR) on every push to main and on version tag releases.
| Tag Pattern | Description | Platforms | Trigger |
|---|---|---|---|
latest |
Latest main branch build |
linux/amd64 |
Push to main
|
sha-<12 chars> |
Specific commit SHA | linux/amd64 |
Every push to main
|
v* (e.g., v1.0.0) |
Semantic version release |
linux/amd64, linux/arm64
|
Version tag push |
# Pull latest development build
docker pull ghcr.io/zeroclaw-labs/zeroclaw:latest
# Pull specific version
docker pull ghcr.io/zeroclaw-labs/zeroclaw:v1.0.0
# Pull specific commit
docker pull ghcr.io/zeroclaw-labs/zeroclaw:sha-a1b2c3d4e5f6All published images have public visibility, allowing unauthenticated pulls. The publishing workflow verifies anonymous access after each push by:
- Requesting an anonymous token from GHCR:
https://ghcr.io/token?scope=repository:${REPOSITORY}:pull - Pulling the manifest with the anonymous token
- Failing the workflow if anonymous access is denied
Sources: .github/workflows/pub-docker-img.yml:169-192
The Docker image publishing pipeline is defined in .github/workflows/pub-docker-img.yml and runs in two modes:
sequenceDiagram
participant PR as "Pull Request Event"
participant Job as "pr-smoke Job"
participant Builder as "Blacksmith Builder"
participant Image as "Local Image"
PR->>Job: "Trigger on Docker path changes"
Job->>Builder: "Setup Docker builder"
Builder->>Builder: "Build image (no push)"
Builder->>Image: "Load as zeroclaw-pr-smoke:latest"
Image->>Job: "Verify: docker run --version"
Job->>PR: "Report success/failure"
The PR smoke job runs when Docker-related files change and verifies that the image builds and executes correctly:
- name: Build smoke image
uses: useblacksmith/build-push-action@30c71162f16ea2c27c3e21523255d209b8b538c1
with:
context: .
push: false
load: true
tags: zeroclaw-pr-smoke:latest
platforms: linux/amd64
- name: Verify image
run: docker run --rm zeroclaw-pr-smoke:latest --versionAverage runtime: ~240 seconds
Sources: .github/workflows/pub-docker-img.yml:44-80
flowchart TD
Trigger["Push to main or tag v*"]
Trigger --> Login["Log in to ghcr.io with GITHUB_TOKEN"]
Login --> ComputeTags["Compute image tags"]
ComputeTags --> CheckRef{"Ref type?"}
CheckRef -->|"refs/tags/v*"| TagFlow["Tags: vX.Y.Z, sha-<12>"]
CheckRef -->|"refs/heads/main"| MainFlow["Tags: latest, sha-<12>"]
CheckRef -->|"other"| BranchFlow["Tags: branch-name, sha-<12>"]
TagFlow --> CheckPlatform{"Ref type?"}
MainFlow --> CheckPlatform
BranchFlow --> CheckPlatform
CheckPlatform -->|"refs/tags/v*"| MultiPlatform["Build: linux/amd64, linux/arm64"]
CheckPlatform -->|"other"| SinglePlatform["Build: linux/amd64"]
MultiPlatform --> Push["Push to ghcr.io"]
SinglePlatform --> Push
Push --> SetVisibility["Set package visibility to public"]
SetVisibility --> VerifyAnonymous["Verify anonymous pull access"]
Tag computation logic at .github/workflows/pub-docker-img.yml:104-123:
- For tag pushes:
${TAG_NAME}andsha-${GITHUB_SHA::12} - For
mainpushes:latestandsha-${GITHUB_SHA::12} - For other branches:
${BRANCH_NAME}andsha-${GITHUB_SHA::12}
Platform matrix:
- Version tag releases (
v*): Build for bothlinux/amd64andlinux/arm64 - All other pushes: Build only for
linux/amd64
Average runtime: ~140 seconds
Sources: .github/workflows/pub-docker-img.yml:82-193
The publishing workflow is path-filtered and only runs when Docker-related files change:
paths:
- "Dockerfile"
- ".dockerignore"
- "Cargo.toml"
- "Cargo.lock"
- "rust-toolchain.toml"
- "src/**"
- "crates/**"
- "benches/**"
- "firmware/**"
- "dev/config.template.toml"
- ".github/workflows/pub-docker-img.yml"This prevents unnecessary image builds when only documentation or tests change.
Sources: .github/workflows/pub-docker-img.yml:7-18
The provided docker-compose.yml offers a production-ready deployment configuration with resource limits, health checks, and volume persistence.
# Create .env file with API key
echo "API_KEY=your_openrouter_key_here" > .env
# Start the service
docker compose up -d
# View logs
docker compose logs -f zeroclaw
# Stop the service
docker compose downgraph LR
subgraph "docker-compose.yml"
Service["zeroclaw service"]
Env["Environment Variables"]
Volumes["Named Volumes"]
Ports["Port Mapping"]
Deploy["Resource Limits"]
Health["Health Check"]
Service --> Env
Service --> Volumes
Service --> Ports
Service --> Deploy
Service --> Health
end
subgraph "Runtime Bindings"
EnvVars["API_KEY, PROVIDER, ZEROCLAW_ALLOW_PUBLIC_BIND"]
VolumeMount["zeroclaw-data:/zeroclaw-data"]
PortBind["HOST_PORT:3000 -> container:3000"]
CPUMem["CPU: 0.5-2 cores, Memory: 512M-2G"]
HealthCmd["zeroclaw status every 60s"]
end
Env -.-> EnvVars
Volumes -.-> VolumeMount
Ports -.-> PortBind
Deploy -.-> CPUMem
Health -.-> HealthCmd
Sources: docker-compose.yml:1-63
| Variable | Purpose | Default | Required |
|---|---|---|---|
API_KEY |
LLM provider API key | None | Yes (for cloud providers) |
PROVIDER |
LLM provider name | openrouter |
No |
ZEROCLAW_MODEL |
Override model selection | From config.toml | No |
ZEROCLAW_ALLOW_PUBLIC_BIND |
Allow binding to [::] in Docker |
true |
Yes (Docker networking) |
HOST_PORT |
External port mapping | 3000 |
No |
Important: ZEROCLAW_ALLOW_PUBLIC_BIND=true is required for Docker networking. The gateway binds to [::] inside the container but is only accessible from the host via the port mapping. For actual public exposure, you need to configure firewall rules or use a reverse proxy.
Sources: docker-compose.yml:18-33
The Docker Compose configuration uses a named volume to persist:
- Agent workspace:
/zeroclaw-data/workspace - Configuration file:
/zeroclaw-data/.zeroclaw/config.toml - SQLite database:
/zeroclaw-data/.zeroclaw/memory.db(if using SQLite memory backend) - Secrets:
/zeroclaw-data/.zeroclaw/secrets.encrypted(if using encrypted secrets)
Volume declaration at docker-compose.yml:61-62:
volumes:
zeroclaw-data:To inspect the volume:
# List volumes
docker volume ls
# Inspect volume details
docker volume inspect zeroclaw_zeroclaw-data
# Backup volume
docker run --rm -v zeroclaw_zeroclaw-data:/data -v $(pwd):/backup ubuntu tar czf /backup/zeroclaw-backup.tar.gz -C /data .
# Restore volume
docker run --rm -v zeroclaw_zeroclaw-data:/data -v $(pwd):/backup ubuntu tar xzf /backup/zeroclaw-backup.tar.gz -C /dataSources: docker-compose.yml:34-36, Dockerfile:76, Dockerfile:99
The Compose file defines resource constraints to prevent runaway resource consumption:
deploy:
resources:
limits:
cpus: '2'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M- Reservations: Guaranteed minimum resources (0.5 CPUs, 512 MB RAM)
- Limits: Hard caps on resource usage (2 CPUs, 2 GB RAM)
Adjust these values based on your workload. Heavy browser automation or large context windows may require higher memory limits.
Sources: docker-compose.yml:43-50
The Docker Compose health check uses the zeroclaw status command to monitor service health:
healthcheck:
test: ["CMD", "zeroclaw", "status"]
interval: 60s
timeout: 10s
retries: 3
start_period: 10sThis verifies that the ZeroClaw process is running and responsive. The health status is visible via docker compose ps:
$ docker compose ps
NAME IMAGE STATUS PORTS
zeroclaw ghcr.io/zeroclaw-labs/zeroclaw Up (healthy) 2 minutes 0.0.0.0:3000->3000/tcpNote: The dev image includes zeroclaw in the PATH. The release (distroless) image also includes it, as distroless provides the necessary runtime libraries for the binary.
Sources: docker-compose.yml:53-59
ZeroClaw configuration follows a three-tier precedence model:
- Environment variables (highest priority)
- config.toml file
- Built-in defaults (lowest priority)
When running in Docker, environment variables can override config.toml settings without requiring container rebuilds.
Scenario 1: Switch provider at runtime
docker compose up -d -e PROVIDER=anthropic -e API_KEY=sk-ant-xxxScenario 2: Use local Ollama
services:
zeroclaw:
environment:
- PROVIDER=ollama
- API_KEY=http://host.docker.internal:11434Scenario 3: Multi-model deployment
services:
zeroclaw-sonnet:
image: ghcr.io/zeroclaw-labs/zeroclaw:latest
environment:
- PROVIDER=anthropic
- ZEROCLAW_MODEL=claude-sonnet-4-20250514
- API_KEY=${ANTHROPIC_API_KEY}
ports:
- "3000:3000"
zeroclaw-opus:
image: ghcr.io/zeroclaw-labs/zeroclaw:latest
environment:
- PROVIDER=anthropic
- ZEROCLAW_MODEL=claude-opus-4-20250514
- API_KEY=${ANTHROPIC_API_KEY}
ports:
- "3001:3000"Sources: Dockerfile:74-84, Dockerfile:98-106, docker-compose.yml:18-33
Build only the dev stage for local development:
docker build --target dev -t zeroclaw:dev .
docker run --rm -it zeroclaw:dev shellBuild the release stage explicitly:
docker build --target release -t zeroclaw:prod .To use a different base image for the dev stage, modify Dockerfile:59:
FROM debian:bookworm-slim AS devFor the release stage, you can replace the distroless image with Alpine for a shell-accessible minimal image:
FROM alpine:3.19 AS release
RUN apk add --no-cache ca-certificatesWarning: Changing base images may affect security posture and dependency compatibility.
The Dockerfile does not currently expose build arguments, but you can add them for customization:
ARG RUST_VERSION=1.93
FROM rust:${RUST_VERSION}-slim AS builderThen build with:
docker build --build-arg RUST_VERSION=1.94 -t zeroclaw:latest .Sources: Dockerfile:4, Dockerfile:59, Dockerfile:93
Symptom: cargo build fails with dependency resolution errors
Solution: Clear Docker build cache and rebuild:
docker builder prune -f
docker build --no-cache -t zeroclaw:latest .Symptom: strip: error while loading shared libraries
Solution: Ensure binutils is installed in the builder stage (already included in rust:*-slim).
Symptom: Container exits immediately with "API key required"
Solution: Provide API_KEY environment variable:
docker run -e API_KEY=your_key_here ghcr.io/zeroclaw-labs/zeroclaw:latestSymptom: Gateway returns "connection refused" from host
Solution: Verify ZEROCLAW_ALLOW_PUBLIC_BIND=true and port mapping:
docker compose logs zeroclaw | grep "Gateway started"
curl http://localhost:3000/healthSymptom: Permission denied writing to /zeroclaw-data/workspace
Solution: Volume is owned by UID 65534. If mounting a host directory, adjust permissions:
sudo chown -R 65534:65534 /host/path/to/workspaceOr run as root (not recommended):
services:
zeroclaw:
user: "0:0" # Run as rootSymptom: Container marked as "unhealthy" in docker compose ps
Solution: Inspect health check logs:
docker inspect --format='{{json .State.Health}}' zeroclaw | jqIf zeroclaw status is failing, verify the binary is accessible:
docker compose exec zeroclaw zeroclaw --versionSources: docker-compose.yml:53-59
Both the dev and release images run as user 65534:65534 (the nonroot user), adhering to the principle of least privilege. This prevents container breakout attacks from gaining root access on the host.
User configuration:
- Dockerfile:87 (dev stage)
- Dockerfile:109 (release stage)
The default Docker Compose configuration does not define a custom network, placing the container on the default bridge network. For production deployments with multiple services, define an isolated network:
services:
zeroclaw:
networks:
- zeroclaw-net
networks:
zeroclaw-net:
driver: bridgeDo not embed API keys in the Dockerfile or Compose file. Use:
-
Environment files (
.env):echo "API_KEY=sk-xxx" > .env docker compose up -d
-
Docker secrets (Swarm mode):
services: zeroclaw: secrets: - api_key secrets: api_key: external: true
-
External secret providers (Vault, AWS Secrets Manager, etc.): Inject secrets at runtime via init containers or sidecar patterns.
For the ZeroClaw secret encryption system, see Secret Management.
Scan published images for vulnerabilities:
docker scan ghcr.io/zeroclaw-labs/zeroclaw:latestOr use Trivy:
trivy image ghcr.io/zeroclaw-labs/zeroclaw:latestThe CI/CD pipeline includes sec-audit.yml which runs on every push, but does not currently scan Docker images. Consider adding this to the publishing workflow.
Sources: docker-compose.yml:18-22, Dockerfile:87, Dockerfile:109
The ZeroClaw Docker deployment system provides:
- Multi-stage builds with aggressive caching for fast iteration
-
Two image variants:
dev(Debian, debugging tools) andrelease(distroless, minimal) - Automated CI/CD via GitHub Actions with PR smoke testing and GHCR publishing
- Production-ready Compose configuration with resource limits, health checks, and volume persistence
- Flexible configuration via environment variable overrides
For next steps:
- Configure production settings: Production Configuration
- Set up multiple channels: Channel Implementations
- Configure memory backends: Memory Backends
Sources: Dockerfile:1-113, docker-compose.yml:1-63, .github/workflows/pub-docker-img.yml:1-193