๐Ÿ”
AdvancedChapter 27 of 33ยท 7 min read

Chapter 27: Self-Hosting with Docker

Running OpenClaw in Docker gives you a production-grade deployment with proper isolation, easy updates, and straightforward management. This chapter covers the official Docker image, Docker Compose setups, networking, persistent storage, Nginx reverse proxy, SSL, monitoring, backups, and troubleshooting.


Why Docker?

Running OpenClaw directly on a host works fine for local development. But for a production server that runs 24/7, Docker gives you:

  • Isolation โ€” OpenClaw and its dependencies are contained; nothing conflicts with other software on the host
  • Reproducibility โ€” The exact same image runs on any machine
  • Easy updates โ€” Pull a new image and restart; no dependency management
  • Rollback โ€” Pin to a previous image tag if an update causes problems
  • Resource limits โ€” Cap CPU and RAM so one runaway process can't take down the whole server

The Official Docker Image

OpenClaw publishes an official multi-arch Docker image:

docker pull openclaw/openclaw:latest

Available tags:

TagDescription
latestLatest stable release
1.4Major.minor pinned
1.4.2Exact version pinned (recommended for production)
nightlyLatest build from main branch (unstable)

The image is built for linux/amd64 and linux/arm64 (including Raspberry Pi 4/5 and Apple Silicon in Linux VMs).

Check the current digest before deploying:

docker pull openclaw/openclaw:1.4.2
docker inspect openclaw/openclaw:1.4.2 --format '{{.Id}}'

Quick Start

docker run -d \
  --name openclaw \
  --restart unless-stopped \
  -p 3000:3000 \
  -v ~/.openclaw:/app/data \
  -e ANTHROPIC_API_KEY=sk-ant-... \
  -e TELEGRAM_BOT_TOKEN=1234567890:ABC... \
  openclaw/openclaw:1.4.2

This mounts your ~/.openclaw directory into the container so your config and data persist across container restarts. The --restart unless-stopped flag means Docker automatically restarts the container after a crash or reboot.


Docker Compose (Recommended)

For a complete production setup with Redis for session storage:

# docker-compose.yml
version: '3.8'

services:
  openclaw:
    image: openclaw/openclaw:1.4.2
    container_name: openclaw
    restart: unless-stopped
    ports:
      - "3000:3000"
    volumes:
      - ./config:/app/config:ro
      - openclaw-data:/app/data
    environment:
      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
      - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
      - SLACK_BOT_TOKEN=${SLACK_BOT_TOKEN}
      - REDIS_URL=redis://redis:6379
      - OPENCLAW_CONFIG=/app/config/openclaw.json
    depends_on:
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/api/v1/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  redis:
    image: redis:7-alpine
    container_name: openclaw-redis
    restart: unless-stopped
    volumes:
      - redis-data:/data
    command: redis-server --appendonly yes
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  openclaw-data:
  redis-data:

Common commands:

# Start all services
docker compose up -d

# Stop all services
docker compose down

# View logs (follow)
docker compose logs -f openclaw

# View Redis logs
docker compose logs -f redis

# Restart only OpenClaw
docker compose restart openclaw

# Check status
docker compose ps

Configuration File Mount

Separate your secrets from your config. Place openclaw.json in ./config/ and keep secrets in .env:

project/
โ”œโ”€โ”€ docker-compose.yml
โ”œโ”€โ”€ .env                  โ† secrets (never commit to git)
โ””โ”€โ”€ config/
    โ””โ”€โ”€ openclaw.json     โ† config (safe to commit without secrets)

.env file:

ANTHROPIC_API_KEY=sk-ant-...
TELEGRAM_BOT_TOKEN=1234567890:ABC...
SLACK_BOT_TOKEN=xoxb-...
REDIS_PASSWORD=your-secure-redis-password

openclaw.json references env vars with ${}:

{
  "agents": {
    "default": {
      "provider": "anthropic",
      "apiKey": "${ANTHROPIC_API_KEY}"
    }
  },
  "channels": {
    "telegram": {
      "enabled": true,
      "token": "${TELEGRAM_BOT_TOKEN}"
    }
  }
}

Reverse Proxy with Nginx

For HTTPS and a custom domain, put Nginx in front of OpenClaw:

# /etc/nginx/sites-available/openclaw
server {
    listen 80;
    server_name bot.yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name bot.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/bot.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/bot.yourdomain.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    # Increase timeout for long AI responses
    proxy_read_timeout 120s;
    proxy_send_timeout 120s;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}

Enable the site and get a free SSL certificate:

sudo ln -s /etc/nginx/sites-available/openclaw /etc/nginx/sites-enabled/
sudo certbot --nginx -d bot.yourdomain.com
sudo nginx -t && sudo systemctl reload nginx

Certbot will auto-renew your certificate every 90 days.


Persistent Storage

The container writes data to /app/data. Always mount a named volume:

volumes:
  - openclaw-data:/app/data

What lives in /app/data:

PathContent
data/sessions/Session files (when using file storage)
data/uploads/User-uploaded files and attachments
data/canvas/Saved canvas sessions
data/memory/Memory skill data
data/logs/Application audit logs
data/skills/Installed custom skills

Never delete this volume unless you intend to reset all data. Back it up regularly.


Backups

Back up your volume nightly with a simple cron job:

# /etc/cron.d/openclaw-backup
0 3 * * * root docker run --rm \
  -v openclaw-data:/data:ro \
  -v /backups/openclaw:/backup \
  alpine tar czf /backup/openclaw-$(date +%Y%m%d).tar.gz -C /data .

This creates a compressed archive of all data every night at 3 AM. Keep 30 days of backups:

# Clean up old backups
find /backups/openclaw -name "*.tar.gz" -mtime +30 -delete

Restore from backup:

docker run --rm \
  -v openclaw-data:/data \
  -v /backups/openclaw:/backup \
  alpine tar xzf /backup/openclaw-20260430.tar.gz -C /data

Updates

# Pull the new image
docker compose pull

# Recreate containers (brief restart)
docker compose up -d --remove-orphans

# Verify the new version
docker compose exec openclaw openclaw --version

Automatic Updates with Watchtower

services:
  watchtower:
    image: containrrr/watchtower
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command: --interval 86400 --cleanup openclaw
    restart: unless-stopped

Watchtower checks for new images every 24 hours and updates automatically. Use --interval 86400 (daily) rather than more frequent checks to avoid unnecessary pulls.

Warning: Automatic updates can break things. For production, consider disabling Watchtower and doing manual updates so you can read the changelog first.


Health Monitoring

The container exposes a health endpoint at /api/v1/health:

curl http://localhost:3000/api/v1/health
{
  "status": "ok",
  "version": "1.4.2",
  "uptime": 86400,
  "channels": {
    "telegram": "connected",
    "slack": "connected"
  },
  "redis": "connected",
  "memoryMB": 128
}

Uptime Kuma (Recommended)

Add a new monitor in Uptime Kuma:

  • Type: HTTP(S)
  • URL: http://localhost:3000/api/v1/health
  • Interval: 60 seconds
  • Expected status code: 200

Prometheus

OpenClaw exposes Prometheus metrics at /metrics:

# prometheus.yml
scrape_configs:
  - job_name: 'openclaw'
    static_configs:
      - targets: ['openclaw:3000']
    scrape_interval: 15s

Key metrics:

MetricDescription
openclaw_messages_totalTotal messages processed
openclaw_tokens_totalTotal tokens used (in/out)
openclaw_active_sessionsCurrent active sessions
openclaw_response_latency_secondsResponse time histogram
openclaw_errors_totalTotal errors by type

Resource Limits

Set resource limits to prevent runaway containers from affecting the host:

services:
  openclaw:
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 128M
  redis:
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 256M

Typical resource usage:

  • Idle: ~50 MB RAM, <1% CPU
  • Active (few users): ~100-200 MB RAM, 10-30% CPU
  • Under load: ~300-400 MB RAM, 50-80% CPU

Firewall Configuration

Only expose the ports you need:

# Allow SSH
ufw allow 22/tcp

# Allow HTTPS (Nginx handles this, not OpenClaw directly)
ufw allow 443/tcp
ufw allow 80/tcp

# Block direct access to OpenClaw port from outside
# (Nginx proxies it internally)
ufw deny 3000/tcp

ufw enable

Troubleshooting

Container keeps restarting:

docker compose logs --tail 50 openclaw

Look for Error: lines. Common causes: missing API key, wrong config path, port already in use.

Can't connect to Telegram/Slack:

docker compose exec openclaw openclaw channels status

Check that env vars are set correctly inside the container:

docker compose exec openclaw env | grep TOKEN

Redis connection refused:

docker compose exec redis redis-cli ping

If it returns PONG, Redis is running. Check REDIS_URL in your .env.

Out of memory:

docker stats openclaw

Increase the memory limit in docker-compose.yml or reduce maxContextTokens in your agent config.


Next: Chapter 28 โ€” Multi-Workspace Deployments โ€” Advanced patterns for organizations running many workspaces, teams, and use cases on a single gateway.