Skip to content

13.1 Docker Deployment

Nikolay Vyahhi edited this page Feb 19, 2026 · 2 revisions

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.

Purpose and Scope

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.


Multi-Stage Build Architecture

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
Loading

Sources: Dockerfile:1-113

Stage 1: Builder

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

Stage 2: Development Runtime (dev)

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

Stage 3: Production Runtime (release)

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


Building Images Locally

Building the Production Image

docker build -t zeroclaw:latest .

This builds the release stage by default (the last stage in the Dockerfile).

Building the Development Image

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.

Build Cache Optimization

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


Using Published Images

ZeroClaw images are automatically published to GitHub Container Registry (GHCR) on every push to main and on version tag releases.

Image Tags

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

Pulling Published Images

# 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-a1b2c3d4e5f6

Anonymous Pull Access

All published images have public visibility, allowing unauthenticated pulls. The publishing workflow verifies anonymous access after each push by:

  1. Requesting an anonymous token from GHCR: https://ghcr.io/token?scope=repository:${REPOSITORY}:pull
  2. Pulling the manifest with the anonymous token
  3. Failing the workflow if anonymous access is denied

Sources: .github/workflows/pub-docker-img.yml:169-192


CI/CD Publishing Workflow

The Docker image publishing pipeline is defined in .github/workflows/pub-docker-img.yml and runs in two modes:

PR Smoke Testing

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"
Loading

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 --version

Average runtime: ~240 seconds

Sources: .github/workflows/pub-docker-img.yml:44-80

Production Publishing

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"]
Loading

Tag computation logic at .github/workflows/pub-docker-img.yml:104-123:

  • For tag pushes: ${TAG_NAME} and sha-${GITHUB_SHA::12}
  • For main pushes: latest and sha-${GITHUB_SHA::12}
  • For other branches: ${BRANCH_NAME} and sha-${GITHUB_SHA::12}

Platform matrix:

  • Version tag releases (v*): Build for both linux/amd64 and linux/arm64
  • All other pushes: Build only for linux/amd64

Average runtime: ~140 seconds

Sources: .github/workflows/pub-docker-img.yml:82-193

Workflow Triggers

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


Docker Compose Deployment

The provided docker-compose.yml offers a production-ready deployment configuration with resource limits, health checks, and volume persistence.

Basic Deployment

# 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 down

Docker Compose Configuration Breakdown

graph 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
Loading

Sources: docker-compose.yml:1-63

Environment Variables

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

Volume Persistence

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 /data

Sources: docker-compose.yml:34-36, Dockerfile:76, Dockerfile:99

Resource Limits

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

Health Check

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: 10s

This 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/tcp

Note: 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


Configuration Override Patterns

Environment Variable Precedence

ZeroClaw configuration follows a three-tier precedence model:

  1. Environment variables (highest priority)
  2. config.toml file
  3. Built-in defaults (lowest priority)

When running in Docker, environment variables can override config.toml settings without requiring container rebuilds.

Common Override Scenarios

Scenario 1: Switch provider at runtime

docker compose up -d -e PROVIDER=anthropic -e API_KEY=sk-ant-xxx

Scenario 2: Use local Ollama

services:
  zeroclaw:
    environment:
      - PROVIDER=ollama
      - API_KEY=http://host.docker.internal:11434

Scenario 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


Advanced Topics

Multi-Stage Build Target Selection

Build only the dev stage for local development:

docker build --target dev -t zeroclaw:dev .
docker run --rm -it zeroclaw:dev shell

Build the release stage explicitly:

docker build --target release -t zeroclaw:prod .

Custom Base Images

To use a different base image for the dev stage, modify Dockerfile:59:

FROM debian:bookworm-slim AS dev

For 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-certificates

Warning: Changing base images may affect security posture and dependency compatibility.

Build Arguments

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 builder

Then build with:

docker build --build-arg RUST_VERSION=1.94 -t zeroclaw:latest .

Sources: Dockerfile:4, Dockerfile:59, Dockerfile:93


Troubleshooting

Build Failures

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).

Runtime Failures

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:latest

Symptom: 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/health

Symptom: 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/workspace

Or run as root (not recommended):

services:
  zeroclaw:
    user: "0:0"  # Run as root

Health Check Failures

Symptom: Container marked as "unhealthy" in docker compose ps

Solution: Inspect health check logs:

docker inspect --format='{{json .State.Health}}' zeroclaw | jq

If zeroclaw status is failing, verify the binary is accessible:

docker compose exec zeroclaw zeroclaw --version

Sources: docker-compose.yml:53-59


Security Considerations

Nonroot User

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:

Network Isolation

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: bridge

Secret Management

Do not embed API keys in the Dockerfile or Compose file. Use:

  1. Environment files (.env):

    echo "API_KEY=sk-xxx" > .env
    docker compose up -d
  2. Docker secrets (Swarm mode):

    services:
      zeroclaw:
        secrets:
          - api_key
    
    secrets:
      api_key:
        external: true
  3. 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.

Image Scanning

Scan published images for vulnerabilities:

docker scan ghcr.io/zeroclaw-labs/zeroclaw:latest

Or use Trivy:

trivy image ghcr.io/zeroclaw-labs/zeroclaw:latest

The 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


Summary

The ZeroClaw Docker deployment system provides:

  1. Multi-stage builds with aggressive caching for fast iteration
  2. Two image variants: dev (Debian, debugging tools) and release (distroless, minimal)
  3. Automated CI/CD via GitHub Actions with PR smoke testing and GHCR publishing
  4. Production-ready Compose configuration with resource limits, health checks, and volume persistence
  5. Flexible configuration via environment variable overrides

For next steps:

Sources: Dockerfile:1-113, docker-compose.yml:1-63, .github/workflows/pub-docker-img.yml:1-193


Clone this wiki locally