← Back to Blog

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

This post documents the full process of deploying the OpenClaw Agent on macOS Docker Desktop, including Tailscale sidecar networking, Gateway configuration, Dashboard connection, and solutions to common issues.

Background

OpenClaw is an AI Agent framework open-sourced at the end of 2025, with over 145k stars on GitHub. It features a Gateway (persistent daemon) + CLI (on-demand interaction) architecture, supports multi-model integration, scheduled Heartbeat tasks, persistent Memory, and other enterprise-grade features.

The official Docker image is alpine/openclaw:latest, but real-world deployment often runs into issues not fully covered by the docs. This post is a hands-on guide after "stepping on every rake."

Environment

  • macOS + Docker Desktop
  • Tailscale for accessing internal services (e.g., self-hosted Gitea)
  • OpenAI API (gpt-4o)

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:

  • trustedProxies: ["0.0.0.0/0"] -- only controls whether proxy headers are trusted, does not bypass pairing

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

The real challenges in deploying OpenClaw with Docker aren't with the Agent itself, but with networking and authentication:

  1. Tailscale sidecar DNS is unreliable -- fix with hardcoded extra_hosts
  2. Docker NAT causes Dashboard pairing failures -- fix with controlUi.allowInsecureAuth
  3. Config format migration -- watch for agent.* to agents.defaults.* changes
  4. Entrypoint isn't a binary -- always use node dist/index.js

The most important lesson: Don't over-engineer. Start with the official image and minimal config, verify each step works, then incrementally add features. Trying to set up all configs, toolchains, and automation at once only makes troubleshooting harder.