Article · 2026-02-25

Deploying OpenClaw with Docker: A Complete Journey from Pitfalls to Success

Background

OpenClaw, open-sourced in late 2025, is an AI Agent framework with over 145k GitHub stars. It combines a persistent Gateway daemon with on-demand CLI interaction, supports multiple models, scheduled tasks, persistent memory, and other enterprise features.

The official Docker image alpine/openclaw:latest deploys readily, but macOS Docker Desktop surfaces several pitfalls the documentation doesn't fully cover: Tailscale DNS failures, Docker NAT blocking the Dashboard, config format changes, and authentication edge cases.

Environment

Architecture Overview

┌─────────────────────────────────────────────┐
│  Docker Compose                             │
│                                             │
│  ┌───────────┐    network_mode:service      │
│  │ Tailscale │◄────────────────────────┐    │
│  │ Sidecar   │    shared network ns     │    │
│  └─────┬─────┘                         │    │
│        │ ports: 18790:18790            │    │
│        │                         ┌─────┴──┐ │
│        │                         │OpenClaw│ │
│        │                         │Gateway │ │
│        │                         └────────┘ │
│        │                                    │
│  ┌─────┴──────┐                             │
│  │ OpenClaw   │  profiles: [cli]            │
│  │ CLI        │  on-demand                  │
│  └────────────┘                             │
└─────────────────────────────────────────────┘

Key decision: The Gateway shares the Tailscale container's network stack via network_mode: "service:tailscale", enabling direct access to services inside the Tailnet.

docker-compose.yml

services:
  tailscale:
    image: tailscale/tailscale:latest
    container_name: my-ts
    hostname: my-agent
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
      - NET_RAW
    devices:
      - /dev/net/tun:/dev/net/tun
    volumes:
      - ts-state:/var/lib/tailscale
    env_file:
      - ./secrets/.env
    environment:
      - TS_STATE_DIR=/var/lib/tailscale
      - TS_USERSPACE=false
      - TS_ACCEPT_DNS=true
    ports:
      - "18790:18790"
    extra_hosts:
      - "my-server:100.x.x.x"  # Hardcoded Tailscale IP

  openclaw-gateway:
    image: alpine/openclaw:latest
    container_name: my-gateway
    restart: unless-stopped
    depends_on:
      - tailscale
    network_mode: "service:tailscale"
    env_file:
      - ./secrets/.env
    environment:
      HOME: /home/node
      TERM: xterm-256color
    volumes:
      - openclaw-config:/home/node/.openclaw
      - openclaw-workspace:/home/node/.openclaw/workspace
    init: true
    security_opt:
      - no-new-privileges:true
    command:
      ["node", "dist/index.js", "gateway", "--bind", "lan", "--port", "18790"]

  openclaw-cli:
    image: alpine/openclaw:latest
    container_name: my-cli
    env_file:
      - ./secrets/.env
    environment:
      HOME: /home/node
      TERM: xterm-256color
      BROWSER: echo
    volumes:
      - openclaw-config:/home/node/.openclaw
      - openclaw-workspace:/home/node/.openclaw/workspace
    stdin_open: true
    tty: true
    init: true
    entrypoint: ["node", "dist/index.js"]
    profiles:
      - cli

volumes:
  ts-state:
  openclaw-config:
  openclaw-workspace:

Pitfall Log

Pitfall 1: Image Name and Entrypoint

The official Docker Hub image is alpine/openclaw, not openclaw/openclaw.

There's no standalone openclaw binary in the container. It's a Node.js app, with entrypoints:

node dist/index.js          # CLI mode
node dist/index.js gateway   # Gateway mode

See the official docker-compose.yml for confirmation.

Pitfall 2: File Permissions (EACCES)

When a Docker named volume is created, the directory is owned by root, but OpenClaw runs as the node user (uid 1000):

EACCES: permission denied, open '/home/node/.openclaw/.env'

Solution:

docker compose run --rm -u root --entrypoint sh openclaw-cli \
  -c "chown -R node:node /home/node/.openclaw"

Note: You must override the entrypoint, or it'll default to running node dist/index.js.

Pitfall 3: Tailscale DNS Not Working

Even with TS_ACCEPT_DNS=true, the container can't resolve Tailscale hostnames:

curl: (6) Could not resolve host: my-server

Root cause: macOS Docker Desktop's /dev/net/tun support is incomplete, so Tailscale's kernel-mode DNS interception may not work.

Solution: Hardcode the Tailscale IP in extra_hosts in docker-compose.yml:

extra_hosts:
  - "my-server:100.x.x.x"

Get the IP from tailscale status. Not elegant, but 100% reliable.

Pitfall 4: Gateway Startup Blocked

Gateway start blocked: set gateway.mode=local (current: unset) or pass --allow-unconfigured.

Solution: Explicitly declare in openclaw.json:

{
  "gateway": {
    "mode": "local"
  }
}

Pitfall 5: Config Format Migration

If you follow old docs, you might write:

{
  "agent": {
    "model": "openai/o3"
  }
}

OpenClaw 2026.2.x will throw:

agent.* was moved; use agents.defaults instead
agent.model string was replaced by agents.defaults.model.primary/fallbacks

Correct format:

{
  "agents": {
    "defaults": {
      "model": {
        "primary": "openai/gpt-4o",
        "fallbacks": ["openai/gpt-4o-mini"]
      }
    }
  }
}

Also, compaction.mode: "aggressive" is no longer valid — just remove it and use the default.

Pitfall 6: The --bind Parameter

Gateway's --bind only accepts these keywords:

Value Meaning
loopback 127.0.0.1, local only
lan 0.0.0.0, required in Docker
tailnet Bind to Tailscale iface
auto Auto-select
custom Specify exact IP

You cannot pass 0.0.0.0 directly — use lan instead.

Pitfall 7: Dashboard "pairing required" (1008)

This is the trickiest. The Dashboard page loads, but the WebSocket disconnects immediately:

disconnected (1008): pairing required

Root cause: The browser accesses via localhost:18790, but after Docker NAT + Tailscale forwarding, the Gateway sees the remote IP as a Tailscale DERP relay IP (e.g., 172.105.x.x), not 127.0.0.1. The Gateway treats this as an external connection and triggers device pairing.

Tried but ineffective:

Final solution: Add to openclaw.json:

{
  "gateway": {
    "controlUi": {
      "enabled": true,
      "allowInsecureAuth": true
    }
  }
}

allowInsecureAuth: true lets the Dashboard accept token-only auth over HTTP, bypassing device identity and pairing checks.

Pitfall 8: Port Conflicts

If another OpenClaw instance on the host is using the default port 18789, the Gateway will fail to start. Use another port (e.g., 18790), and update all three places:

  1. The --port argument in docker-compose.yml command
  2. The ports mapping for tailscale in docker-compose.yml
  3. The gateway.port in openclaw.json

All three must match.

Onboarding Process

The first run requires the onboarding wizard for initialization:

# Stop the gateway first to avoid repeated restarts before init
docker compose stop openclaw-gateway

# Run onboarding
docker compose run --rm openclaw-cli onboard

The wizard will prompt:

Step Recommendation
Security acknowledgment Confirm
QuickStart / Advanced QuickStart
Model provider OpenAI
Default model Choose as needed (gpt-4o is stable, o3 needs org validation)
Channel (WhatsApp/Telegram etc.) Skip
Skill dependencies (API Keys) All No
Hooks Skip for now

Afterwards, start the Gateway:

docker compose up -d

Get the Dashboard URL:

docker compose run --rm openclaw-cli dashboard --no-open

Final openclaw.json

{
  "gateway": {
    "mode": "local",
    "bind": "lan",
    "port": 18790,
    "trustedProxies": ["0.0.0.0/0"],
    "controlUi": {
      "enabled": true,
      "allowInsecureAuth": true
    }
  },
  "agents": {
    "defaults": {
      "model": {
        "primary": "openai/gpt-4o",
        "fallbacks": ["openai/gpt-4o-mini"]
      },
      "heartbeat": {
        "every": "24h",
        "model": "openai/gpt-4o-mini",
        "prompt": "Check workspace for pending tasks. Execute if needed, otherwise reply HEARTBEAT_OK."
      }
    }
  }
}

SSH Configuration Inside the Container (for Git Access)

If you want the Agent to push code to a self-hosted Git service:

# Generate key inside the container
docker compose exec openclaw-gateway \
  ssh-keygen -t ed25519 -C "openclaw@agent" -f /home/node/.ssh/id_ed25519 -N ""

# View public key and add to Git service
docker compose exec openclaw-gateway cat /home/node/.ssh/id_ed25519.pub

# Configure SSH (example: Gitea on port 2222)
docker compose exec openclaw-gateway bash -c 'cat >> /home/node/.ssh/config << EOF
Host my-server
  Port 2222
  User git
  IdentityFile /home/node/.ssh/id_ed25519
EOF'

# Set git user info
docker compose exec openclaw-gateway bash -c \
  'git config --global user.name "openclaw" && git config --global user.email "[email protected]"'

Note: The SSH key is stored in the container's filesystem, not in a named volume. If the container is rebuilt, you'll need to regenerate it. For persistence, mount an extra volume to /home/node/.ssh.

Data Persistence

Volume Container Path Contents
ts-state /var/lib/tailscale Tailscale node state
openclaw-config /home/node/.openclaw Agent config, sessions, memory
openclaw-workspace /home/node/.openclaw/workspace Workspace files

Inject config files (openclaw.json, SOUL.md, etc.) from the host:

docker compose cp config/openclaw.json openclaw-gateway:/home/node/.openclaw/openclaw.json
docker compose cp config/SOUL.md openclaw-gateway:/home/node/.openclaw/workspace/SOUL.md

Quick Reference for Daily Operations

# Start/stop
docker compose up -d
docker compose down

# Enter container
docker compose exec openclaw-gateway bash          # as node user
docker compose exec -u root openclaw-gateway bash  # as root

# CLI operations
docker compose run --rm openclaw-cli status
docker compose run --rm openclaw-cli dashboard --no-open

# View logs
docker compose logs -f openclaw-gateway

# Copy config to container
docker compose cp config/openclaw.json openclaw-gateway:/home/node/.openclaw/openclaw.json
docker compose restart openclaw-gateway

Summary

Deploying OpenClaw on macOS Docker presents specific challenges distinct from the application itself:

  1. Tailscale sidecar DNS is unreliable — use hardcoded extra_hosts instead
  2. Docker NAT breaks Dashboard pairing — enable controlUi.allowInsecureAuth to bypass checks
  3. Config format has migrated — watch for agent.*agents.defaults.* changes
  4. Entrypoint is a Node.js app — always run node dist/index.js

Start with the official image and minimal config, verify each step works, then incrementally add features. Attempting to set up all configs, toolchains, and automation at once only complicates troubleshooting.

© 2026 Yuxu Ge ·