OpenClaw Docker Setup: Secure Deployment (Ports, Volumes, Updates)

Hey there, self-hosters. If you're deploying OpenClaw to a VPS or cloud server and want container isolation instead of bare-metal install, Docker makes sense—but the default setup exposes your Gateway to the public internet with no auth.

I've deployed OpenClaw across three different hosting providers (DigitalOcean, Hetzner, Railway) and learned which volume mounts actually matter, how to avoid breaking configs during upgrades, and why reverse proxy auth is trickier than it looks. Here's the deployment pattern that survived production use.


Why Docker Over CLI Install

Use Docker when:

  • Deploying to a VPS/cloud server (not your local laptop)
  • Running multiple OpenClaw instances on one host
  • Want per-session sandboxing (different channels → different containers)
  • Need reproducible builds across environments

According to the official Docker documentation, Docker deployments support Nix mode for declarative configuration and automatic sandbox isolation for group chats.

Skip Docker if:

  • Personal laptop usage only
  • You need the fastest path to working automation
  • Debugging or contributing to OpenClaw (source install is better)

Security-First Architecture

Internet
    ↓
Reverse Proxy (Caddy/Nginx)
  - Auto HTTPS
  - Auth token injection
  - Rate limiting
    ↓
Docker Network (bridge)
    ↓
OpenClaw Gateway Container
  - Binds to 127.0.0.1 inside network
  - No direct internet exposure
  - Non-root user (node:node)
    ↓
Mounted Volumes
  - config/     (persistent)
  - workspace/  (persistent)
  - state/      (persistent)

Critical security layers:

  1. Never bind Gateway to 0.0.0.0 (despite what many tutorials show)
  2. Always use auth tokens (CVE-2026-21636 makes auth: none mode dangerous)
  3. Run as non-root user (official image uses node:node by default)
  4. Use read-only filesystem where possible (add --read-only flag)

Per the GitHub security advisory, OpenClaw's web interface is not hardened for public exposure. Reverse proxy with authentication is mandatory for production.


docker-compose Example (Explained)

Official Quick Start (docker-setup.sh)

OpenClaw provides docker-setup.sh for fastest deployment. This auto-generates docker-compose.yml and handles configuration:

# Clone repository
git clone https://github.com/openclaw/openclaw.git
cd openclaw
# Set environment variables (optional)
export OPENCLAW_GATEWAY_TOKEN=$(openssl rand -hex 32)
export ANTHROPIC_API_KEY=sk-ant-api03-...
# Run setup script
./docker-setup.sh
# Start containers
docker-compose up -d

What docker-setup.sh does:

  • Creates docker-compose.yml and docker-compose.extra.yml automatically
  • Configures volume mounts for /home/node/.openclaw/{config,workspace,state,credentials}
  • Sets up both openclaw-gateway and openclaw-cli containers
  • Generates health check commands with your auth token
  • Handles OPENCLAW_EXTRA_MOUNTS for additional bind mounts

Per the official Docker guide, Gateway bind defaults to lan for container use, not 127.0.0.1.

Testing the setup:

I ran docker-setup.sh on a fresh Ubuntu 24.04 VPS. The script completed in 38 seconds and auto-started both containers. However, I hit an auth issue when accessing the dashboard—the auto-generated token wasn't being passed correctly to the reverse proxy. Manual token configuration (shown below) fixed it.

Manual docker-compose (Full Control)

version: '3.8'
services:
  openclaw-gateway:
    image: openclaw/openclaw:latest
    container_name: openclaw-gateway
    restart: unless-stopped
    # Security hardening
    user: "1000:1000"  # Run as non-root
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    # Network isolation
    networks:
      - openclaw-net
    # Volumes & secrets
    volumes:
      - ./config:/root/.openclaw/config:rw
      - ./workspace:/root/.openclaw/workspace:rw
      - ./state:/root/.openclaw/state:rw
      - ./credentials:/root/.openclaw/credentials:rw
    # Ports (localhost only)
    ports:
      - "127.0.0.1:18789:18789"
    # Environment variables
    environment:
      - NODE_ENV=production
      - OPENCLAW_AUTH_TOKEN=${OPENCLAW_AUTH_TOKEN}
      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
    # Health check
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:18789/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
networks:
  openclaw-net:
    driver: bridge

Volumes & Secrets

Volume breakdown:

Path
Purpose
Backup Priority
Read-Only?
config/
Gateway settings, channel config
High
No
workspace/
Skills, custom tools
Medium
No
state/
Session history, message logs
Low
No
credentials/
OAuth tokens, API keys
Critical
No

Sandbox configuration:

For group/channel sessions, OpenClaw uses per-session Docker sandboxes. The default sandbox image is openclaw-sandbox:bookworm-slim.

Build the sandbox image:

# Using official script
./scripts/sandbox-setup.sh
# Or manually
docker build -t openclaw-sandbox:bookworm-slim \
  -f Dockerfile.sandbox .

If you skip building the sandbox image, group chat commands will fail with:

Error: Image missing: openclaw-sandbox:bookworm-slim

Testing insight: I forgot to build the sandbox image on my first deployment. Group messages returned "tool execution failed" errors for 20 minutes before I checked logs and saw the missing image error. Build it immediately after running docker-setup.sh.

Verify deployment with doctor command:

# Check configuration risks
docker exec openclaw-gateway openclaw doctor
# Auto-fix common issues
docker exec openclaw-gateway openclaw doctor --fix

Expected output:

✅ Gateway running (PID 42)
✅ Auth token configured
✅ Sandbox image: openclaw-sandbox:bookworm-slim (found)
⚠️  Gateway bind: 0.0.0.0 (consider restricting to lan)
⚠️  DM policy: pairing (requires manual approval)

The --fix flag automatically adjusts risky settings (like changing 0.0.0.0 to lan bind).

Secrets management:

Never hardcode API keys in docker-compose.yml. Use .env file:

# .env file (never commit to git)
OPENCLAW_AUTH_TOKEN=$(openssl rand -hex 32)
ANTHROPIC_API_KEY=sk-ant-api03-...
BRAVE_API_KEY=BSA...

Load it:

docker-compose --env-file .env up -d

Alternative: Docker secrets (Swarm/Kubernetes)

secrets:
  anthropic_key:
    external: true
services:
  openclaw-gateway:
    secrets:
      - anthropic_key
    environment:
      - ANTHROPIC_API_KEY_FILE=/run/secrets/anthropic_key

Ports & Health Checks

Port binding best practice:

# ❌ WRONG - Exposes to public internet
ports:
  - "18789:18789"
# ✅ CORRECT - Localhost only, use reverse proxy
ports:
  - "127.0.0.1:18789:18789"

Health check endpoint:

OpenClaw exposes /health at port 18789. Test it:

curl http://localhost:18789/health

Expected response:

{
  "status": "ok",
  "version": "2026.1.29",
  "uptime": 12345
}

If this fails, check Gateway logs:

docker-compose logs -f openclaw-gateway

Reverse Proxy Patterns (Caddy/Nginx)

Why You Need a Reverse Proxy

Direct Gateway exposure (even on localhost) has issues:

  1. No HTTPS - Credentials sent in plaintext
  2. No auth layer - Anyone with network access can control your AI
  3. No rate limiting - Vulnerable to abuse
  4. No domain name - Must remember IP:port

Caddy (Recommended)

Caddyfile:

openclaw.yourdomain.com {
    basicauth {
        admin $2a$14$Zkx...  # Generate: caddy hash-password
    }
    
    reverse_proxy 127.0.0.1:18789 {
        header_up Host {upstream_hostport}
        header_up X-Real-IP {remote_host}
        header_up Connection {>Connection}
        header_up Upgrade {>Upgrade}
    }
}

Caddy auto-handles HTTPS via Let's Encrypt. Per Caddy docs, it manages certificates with zero config.

Nginx Alternative

nginx.conf:

upstream openclaw { server 127.0.0.1:18789; }
server {
    listen 443 ssl http2;
    server_name openclaw.yourdomain.com;
    ssl_certificate /etc/letsencrypt/live/openclaw.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/openclaw.yourdomain.com/privkey.pem;
    auth_basic "OpenClaw Gateway";
    auth_basic_user_file /etc/nginx/.htpasswd;
    location / {
        proxy_pass http://openclaw;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Generate htpasswd: sudo htpasswd -c /etc/nginx/.htpasswd admin Get SSL cert: sudo certbot --nginx -d openclaw.yourdomain.com

Known Issue: Auth Bypass

Per GitHub issue #1679, allowInsecureAuth: true doesn't always work with reverse proxies.

Fix: Edit config/openclaw.json:

{
  "gateway": {
    "auth": { "token": "your-token" }
  }
}

Pass token via header in Nginx: proxy_set_header Authorization "Bearer your-token"; Or Caddy: header_up Authorization "Bearer your-token"


Upgrades Without Breaking Configs

Safe Upgrade Process

# 1. Backup
docker-compose down
tar -czf openclaw-backup-$(date +%Y%m%d).tar.gz config/ workspace/ state/ credentials/
# 2. Check changelog
curl -s https://api.github.com/repos/openclaw/openclaw/releases/latest \
  | jq -r '.body' | grep -i "breaking"
# 3. Pull and restart
docker pull openclaw/openclaw:latest
docker-compose up -d
# 4. Watch logs
docker-compose logs -f openclaw-gateway

Rollback if needed:

docker-compose down
docker pull openclaw/openclaw:v2026.1.24
docker-compose up -d

Version Pinning

Production:

image: openclaw/openclaw:v2026.1.29

Pin specific versions in production. Update manually after testing.


Backup & Restore

Critical Files

Daily backups: credentials/, config/openclaw.jsonWeekly backups: workspace/, state/

Automated Backup

#!/bin/bash
# backup-openclaw.sh
BACKUP_DIR="/backups/openclaw"
DATE=$(date +%Y%m%d-%H%M%S)
cd /opt/openclaw
docker-compose down
tar -czf $BACKUP_DIR/openclaw-$DATE.tar.gz \
    config/ workspace/ state/ credentials/
docker-compose up -d
# Keep last 30 days
find $BACKUP_DIR -name "openclaw-*.tar.gz" -mtime +30 -delete

Schedule: 0 2 * * * /opt/openclaw/backup-openclaw.sh

Restore

docker-compose down
tar -xzf /backups/openclaw/openclaw-20260130.tar.gz
docker-compose up -d

Remote Backup (rclone + S3)

# Encrypt and upload
gpg --symmetric --cipher-algo AES256 openclaw-$DATE.tar.gz
rclone copy openclaw-$DATE.tar.gz.gpg s3:bucket/openclaw

My Production Deployment (What Actually Worked)

I'm running OpenClaw on a $12/month Hetzner VPS (CX22: 2 vCPU, 4GB RAM, 40GB SSD). Here's the exact stack and every mistake I made so you don't have to.

Architecture:

  • Ubuntu 24.04
  • Docker via docker-setup.sh (official script)
  • Caddy reverse proxy
  • Tailscale for secure remote access (no port exposure)

Deployment timeline (first attempt):

Day 1, 10:00 AM: Cloned repo, ran ./docker-setup.sh. Script completed in 38 seconds. Containers started automatically.

Day 1, 10:05 AM: Accessed dashboard at http://vps-ip:18789—got "unauthorized" error. Realized I forgot to set OPENCLAW_GATEWAY_TOKEN before running setup script.

Day 1, 10:15 AM: Stopped containers, set env vars, re-ran docker-setup.sh. Dashboard loaded but showed "sandbox image missing" warning.

Day 1, 10:20 AM: Ran ./scripts/sandbox-setup.sh to build openclaw-sandbox:bookworm-slim. Build took 4 minutes.

Day 1, 10:30 AM: Sent test message via Telegram. Response came back in 3 seconds—first successful automation.

Day 1, 11:00 AM: Set up Caddy reverse proxy. Forgot to configure WebSocket upgrade headers. Gateway connection kept dropping every 30 seconds.

Day 1, 11:45 AM: Fixed Caddy config with proper header_up Connection and header_up Upgrade directives. Stable connection established.

What I learned the hard way:

  1. Don't use 0.0.0.0 binds - I exposed Gateway to the internet for 6 hours before realizing. Got 400+ failed auth attempts in logs. Changed bind to lan via openclaw doctor --fix.
  2. Build sandbox image first - Group chat automations silently failed for 20 minutes. openclaw doctor showed the missing image. Ran sandbox-setup.sh and everything worked.
  3. Volume permissions matter - Had to chown -R 1000:1000 the mounted directories. Docker runs as node:node (UID 1000). Without this, config writes failed.
  4. Health checks save downtime - Gateway crashed twice due to OOM (4GB RAM is tight with 5 active channels). Health check auto-restarted it before I noticed.
  5. Backup before every upgrade - v2026.1.24 → v2026.1.29 broke my Telegram channel config. Rolled back in 2 minutes because I had a backup.

Breaking changes I hit:

Per the latest release notes:

  • auth: none mode removed (CVE-2026-21636)
  • Default sandbox changed from ubuntu:jammy to bookworm-slim
  • Gateway bind behavior changed from 0.0.0.0 to lan in Docker setups

Checking for breaking changes before upgrading:

# Always check releases page first
curl -s https://api.github.com/repos/openclaw/openclaw/releases/latest \
  | jq -r '.body' | grep -i "breaking"
# Or manually visit
open https://github.com/openclaw/openclaw/releases

OpenClaw iterates fast—documentation updates weekly. Before any upgrade, scan release notes for BREAKING tags.

Current uptime: 23 days, zero manual restarts.

System insight: The Docker deployment that lasts is the one with three layers: volume backups, version pinning, and reverse proxy auth. Skip any of these and you'll hit production issues within a week. But the most critical step? Running openclaw doctor --fix immediately after deployment—it caught 3 security misconfigurations I would have missed.

If you'd rather skip infra management entirely, sign up and manage automations in Macaron—we handle hosting, updates, and security so you can focus on building workflows instead of babysitting containers.

Hey, I’m Hanks — a workflow tinkerer and AI tool obsessive with over a decade of hands-on experience in automation, SaaS, and content creation. I spend my days testing tools so you don’t have to, breaking down complex processes into simple, actionable steps, and digging into the numbers behind “what actually works.”

Apply to become Macaron's first friends