Skip to content

Docker installation guide

Applies to: v1.8.23+ Last updated: 2026-04-22

This guide covers Docker Compose deployment, including Codex device-code login, API keys, credential volumes, channel webhooks, and updates.

If you just want to try DuDuClaw on your own machine, read the native installation steps in docs/deployment-guide.md §1 first; native installation usually starts faster and is easier to debug. Docker is a better fit for server deployments, isolated execution environments, or teams that need a consistent environment.


The DuDuClaw Docker image is built from container/Dockerfile.server using a three-stage build:

Stage Contents
1. frontend-builder node:22-slim + builds the React/TS frontend
2. rust-builder rust:slim + compiles the duduclaw release binary
3. production python:3.12-slim + all three AI CLIs + Docker CLI + runtime

The final image ships with:

  • The duduclaw main binary (Rust, includes the dashboard)
  • @anthropic-ai/claude-code, @openai/codex (installed via npm i -g)
  • The docker.io CLI (a client for the Docker daemon behind the mounted socket; used by the task sandbox when it is enabled)
  • (Optional) Python 3.12: only needed for the LLMLingua-2 prompt compressor (requires llmlingua); skill security scanning and channel replies are already Rust-native and no longer depend on Python

Item Version Notes
Docker Engine ≥ 24.0 On Linux, install the native package directly; on macOS/Windows use Docker Desktop or Colima
Docker Compose v2.20+ Bundled with modern Docker; the command is docker compose, not the legacy docker-compose
Git any For cloning the source
Disk space ≥ 4 GB The build consumes ~3 GB temporarily; the final image is roughly ~1.2 GB
Port 18789 Default gateway port, changeable

If you want channel webhooks (LINE, WhatsApp, Feishu and the other webhook channels) to receive external messages, you also need a publicly reachable HTTPS URL. The simplest options are Tailscale Funnel or Cloudflare Tunnel.


終端機視窗
# 1. Clone the source
git clone https://github.com/zhixuli0406/DuDuClaw.git
cd DuDuClaw
# 2. Create .env (fill in as needed)
cp .env.example .env
$EDITOR .env
# 3. Start
docker compose up -d
# 4. Watch the startup logs
docker compose logs -f duduclaw
# 5. Verify
curl http://localhost:18789/health
# {"status":"ok","version":"1.8.23", ...}
# 6. Open the dashboard
open http://localhost:18789

On first boot, server-entrypoint.sh detects that ~/.duduclaw/config.toml doesn’t exist and automatically runs duduclaw onboard --yes to generate a base config file (API keys and channel tokens are stored encrypted).


docker-compose.yml only reads variables that are actually defined — an unset variable is passed through as an empty string (every reference uses the ${VAR:-} syntax). The list below is grouped by purpose.

4.1 Runtime authentication (set at least one)

Section titled “4.1 Runtime authentication (set at least one)”
Variable Purpose Notes
CLAUDE_CODE_OAUTH_TOKEN Claude Code long-lived token Generated by claude setup-token; fits browser-less container environments. A subscription credential: read the risk note in section 6 first
ANTHROPIC_API_KEY Claude API key Fallback, billed by usage
OPENAI_API_KEY Codex / OpenAI-compat API key Fallback; ChatGPT Plus/Pro OAuth goes through a volume instead
GEMINI_API_KEY Gemini API key Fallback; Google OAuth goes through a volume instead

At least one of these needs to be available. DuDuClaw decides which one to use based on agent.toml [runtime] and [model] api_mode. See §6 CLI authentication for details.

Variable Channel
LINE_CHANNEL_TOKEN / LINE_CHANNEL_SECRET LINE Messaging API
TELEGRAM_BOT_TOKEN Telegram bot
DISCORD_BOT_TOKEN Discord bot
SLACK_BOT_TOKEN / SLACK_APP_TOKEN Slack Socket Mode
WHATSAPP_ACCESS_TOKEN and others WhatsApp Cloud API (see .env.example)
FEISHU_APP_ID / FEISHU_APP_SECRET Feishu

Only fill in the channels you actually plan to use. A channel with no configured variables simply doesn’t start.

docker-compose.yml currently only injects the LINE_* / TELEGRAM_* / DISCORD_* variable groups by default. To pass in other channels via environment variables, add them yourself under the environment: block in the compose file, or add them after startup through Dashboard → Channels → Add — a hot-add path that’s recommended since it needs no gateway restart and the token is written to the encrypted config immediately.

Variable Default Description
DUDUCLAW_BIND 0.0.0.0 Gateway listen address. Inside the container this must stay 0.0.0.0, or nothing outside the container can reach it
DUDUCLAW_ALLOWED_ORIGINS (empty) Extra allowed WebSocket/CORS Origin values for the dashboard, comma-separated. Fill this in when the dashboard is served through a tailnet or reverse-proxy domain and the WebSocket returns 403 (see §13 Troubleshooting). Merged with config.toml’s [gateway] allowed_origins

Port is set in the compose file rather than via env var — see §5 Port configuration.

Variable Purpose
TS_AUTHKEY Automatically joins the tailnet on boot for Tailscale Funnel (used with --profile tailscale)

docker-compose.yml:

services:
duduclaw:
ports:
- "18789:18789" # HOST:CONTAINER
environment:
- DUDUCLAW_BIND=0.0.0.0
  • Inside the container: the gateway always binds to 0.0.0.0:18789. This is deliberate — the container is already an isolated network namespace, so binding to localhost would make it unreachable from outside.
  • Host: 18789:18789 forwards host port 18789 to container port 18789.

The most common case is that 18789 is already taken, or you need multiple instances. Just change the number on the left:

ports:
- "28789:18789" # external side now uses 28789

After that, both the dashboard and webhook URLs need to switch to the new port:

終端機視窗
curl http://localhost:28789/health

Don’t touch the right-hand 18789 (the container-internal port). Changing it requires also updating [gateway] port in ~/.duduclaw/config.toml, and since the entrypoint auto-generates that file via --yes, keeping the two in sync is extra maintenance overhead.

5.3 Binding to localhost only (avoid exposing it to the LAN)

Section titled “5.3 Binding to localhost only (avoid exposing it to the LAN)”

The default ports: "18789:18789" binds to 0.0.0.0, so other machines on the same network can connect too. If you only want it reachable locally (for example, behind a reverse proxy):

ports:
- "127.0.0.1:18789:18789"

Running two DuDuClaw instances on the same machine:

終端機視窗
# Directory A — production
cd /srv/duduclaw-prod
# ports: "18789:18789"
docker compose up -d
# Directory B — staging
cd /srv/duduclaw-staging
# ports: "28789:18789"
# also change container_name to avoid a clash
docker compose up -d

Remember to also change container_name (default duduclaw-server) to avoid a naming collision.


The image includes Claude, Codex, Antigravity and Grok. Their login methods differ. Codex uses device authorization, so the browser never needs to reach a callback listener inside Docker. Keep an API key available for provider billing and fallback.

The standard Compose file mounts these credential and settings directories:

Path Volume CLI
/home/duduclaw/.claude duduclaw-claude Claude Code
/home/duduclaw/.codex duduclaw-codex Codex
/home/duduclaw/.gemini duduclaw-gemini Antigravity settings
/home/duduclaw/.grok duduclaw-grok Grok
/home/duduclaw/.kimi-code duduclaw-kimi Kimi Code
/home/duduclaw/.copilot duduclaw-copilot GitHub Copilot CLI
/home/duduclaw/.kiro duduclaw-kiro Kiro CLI
/home/duduclaw/.cursor duduclaw-cursor Cursor CLI
/home/duduclaw/.local/share/opencode duduclaw-opencode OpenCode

Persistent volumes keep CLI state across container replacement. A custom Compose file must mount the relevant directory as well.

The five additional volumes preserve credentials if those CLIs are installed separately. They are not bundled in this image. An uninstalled CLI cannot be logged in from the Dashboard. Cursor browser login still needs a browser callback reachable by the CLI; use API-key authentication on a remote host if that callback cannot be reached.

Subscription logins carry account risk. Anthropic’s legal and compliance page says third-party developers may not offer Claude.ai login in their own applications or route requests through Free, Pro or Max plan credentials on behalf of their users, and may not collect, store or intermediate Claude.ai credentials or session tokens; Anthropic may enforce this without prior notice, and users have reported suspended accounts. Letting one subscription answer channel customers, rotating several subscriptions past the usage limits, and storing the token in DuDuClaw all conflict with it. Google’s Antigravity terms say that accessing its service with third-party software through Antigravity OAuth violates those terms and may lead to suspension or termination of the account. OpenAI runs an official Sign in with ChatGPT program: ChatGPT plan use in third-party apps is offered to open-source partners and selected commercial partners, a commercial product needs OpenAI’s approval, and auth tokens must stay local and user-controlled, not in remote or managed environments. The setup-token, interactive-login and ChatGPT methods below are subscription logins and stay available; an API key is the supported path. The dashboard’s login flows ask you to accept this notice first. See features/13-multi-runtime.md.

Claude Code provides a long-lived token built for automation scenarios — no browser callback required, which fits containers, CI, and headless servers:

終端機視窗
# Run this on your own machine (the one with a browser), not inside the container
npm install -g @anthropic-ai/claude-code
claude setup-token
# Follow the prompts to complete OAuth in your browser
# It prints CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-... at the end

Write the generated token into .env:

.env
CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-xxxxxxxx...

Then:

終端機視窗
docker compose up -d

DuDuClaw reads the environment variable on startup, and the claude CLI recognizes the token and logs in automatically — you don’t need to touch the container at all. The token is valid for 30 days; DuDuClaw shows a warning on the dashboard starting 7 days before it expires.

Method B: interactive login inside the container

Section titled “Method B: interactive login inside the container”

If you have a Pro / Team / Max subscription and want to use its quota:

終端機視窗
# Enter the container
docker compose exec duduclaw bash
# Run inside the container
claude auth login
# It prints a URL — open it in your local browser and complete authorization
# The state is written to /home/duduclaw/.claude/, i.e. the duduclaw-claude volume
# Verify
claude auth status
exit

The state survives container restarts, since it lives on the volume.

.env:

終端機視窗
ANTHROPIC_API_KEY=sk-ant-...

This path is billed by usage and works as a backup once your subscription quota runs out or an account gets rate-limited. DuDuClaw’s AccountRotator automatically rotates across multiple accounts — see features/07-account-rotation.md for details.

終端機視窗
docker compose exec duduclaw bash
codex login --device-auth
# Open the verification URL shown by Codex in a browser and enter its device code.
# Wait for the CLI to report success; do not paste a callback URL into the dashboard.
# State is written to /home/duduclaw/.codex/, i.e. the duduclaw-codex volume
exit

This method uses your ChatGPT subscription quota, with no extra API cost. The token stays in the container volume. OpenAI’s Sign in with ChatGPT terms say persistent storage of auth tokens must be local and user-controlled, not in remote or managed environments, so on a server prefer Method B (see features/13-multi-runtime.md).

.env:

終端機視窗
OPENAI_API_KEY=sk-proj-...

Billed by usage, at OpenAI API pricing.

The Docker image no longer installs Gemini CLI. Antigravity (agy) remains supported, as does the Gemini API provider. agy subscription sign-in needs an OS keyring, so use its API-key mode in a container: set config.toml [antigravity] auth = "api_key" and supply a gemini provider account or GEMINI_API_KEY in .env. Antigravity settings are stored under ~/.gemini/antigravity-cli/; keep the duduclaw-gemini volume for them.

Each agent can specify which runtime to use in its own agent.toml:

~/.duduclaw/agents/my-agent/agent.toml
[runtime]
provider = "claude" # primary runtime: claude / codex / antigravity / openai-compat …
fallback = "antigravity" # automatically switches over when Claude is unavailable

See features/13-multi-runtime.md for a full example and the failover strategy.

If provider is not set, the agent runs on Claude.

終端機視窗
docker compose exec duduclaw bash
# Installed CLIs should print a version
claude --version
codex --version
# Claude login state
claude auth status

docker-compose.yml declares ten named volumes:

Volume Contents Backup recommendation
duduclaw-data Main application data: config, agents, memory SQLite, logs, bus_queue.jsonl Most important, back up daily
duduclaw-claude Claude CLI OAuth state Moderate — losing it just means running claude auth login again
duduclaw-codex Codex OAuth state Moderate
duduclaw-gemini Antigravity settings under ~/.gemini Moderate
duduclaw-grok Grok CLI credentials Moderate
duduclaw-kimi Kimi Code credentials Moderate
duduclaw-copilot Copilot CLI credentials Moderate
duduclaw-kiro Kiro CLI credentials Moderate
duduclaw-cursor Cursor CLI credentials Moderate
duduclaw-opencode OpenCode credentials Moderate

Plus a mounted host path:

Path Purpose Requirement
/var/run/docker.sock Lets the container talk to the host Docker daemon Only for the per-agent task sandbox (off by default); running the task sandbox from a containerised gateway is not covered by task-sandbox.md and has not been verified
終端機視窗
# Back up the main data volume to a tar.gz
docker run --rm \
-v duduclaw_duduclaw-data:/source:ro \
-v $(pwd):/backup \
alpine tar czf /backup/duduclaw-data-$(date +%F).tar.gz -C /source .

Copy the backup to the target machine, then restore it into a volume of the same name:

終端機視窗
docker volume create duduclaw_duduclaw-data
docker run --rm \
-v duduclaw_duduclaw-data:/target \
-v $(pwd):/backup \
alpine tar xzf /backup/duduclaw-data-2026-04-22.tar.gz -C /target

7.3 Replacing volumes with host directories (advanced)

Section titled “7.3 Replacing volumes with host directories (advanced)”

If you’d rather browse the data directly, you can swap named volumes for bind mounts:

volumes:
- ./data/duduclaw:/home/duduclaw/.duduclaw
- ./data/claude:/home/duduclaw/.claude
- ./data/codex:/home/duduclaw/.codex
- ./data/gemini:/home/duduclaw/.gemini
- ./data/grok:/home/duduclaw/.grok
- ./data/kimi:/home/duduclaw/.kimi-code
- ./data/copilot:/home/duduclaw/.copilot
- ./data/kiro:/home/duduclaw/.kiro
- ./data/cursor:/home/duduclaw/.cursor
- ./data/opencode:/home/duduclaw/.local/share/opencode
- /var/run/docker.sock:/var/run/docker.sock

Note that the host directories must be owned by UID 1000 (the duduclaw user inside the container):

終端機視窗
mkdir -p data/{duduclaw,claude,codex,gemini,grok,kimi,copilot,kiro,cursor,opencode}
sudo chown -R 1000:1000 data/

Path Purpose HTTP 200 condition
/health Full status (JSON) Always returns 200 with structured content
/health/ready Readiness probe All agents have finished loading
/health/live Liveness probe The process is still alive

Compose uses /health for its healthcheck by default (every 30s, marked unhealthy after 3 consecutive failures).

終端機視窗
# Container status
docker compose ps
# Live logs
docker compose logs -f duduclaw
# Last 100 lines
docker compose logs --tail=100 duduclaw
# Healthcheck history
docker inspect duduclaw-server --format '{{json .State.Health}}' | jq

The gateway exposes a GET /metrics endpoint; see the metrics list at docs/deployment-guide.md §8.


docker-compose.yml ships with a built-in Watchtower service that checks for image updates once an hour.

Note: the compose file’s current WATCHTOWER_SCOPE=duduclaw setting only updates containers carrying the matching label, but the duduclaw service itself doesn’t have that label set. To enable auto-updates, add this under services.duduclaw:

labels:
- "com.centurylinklabs.watchtower.scope=duduclaw"

Or remove WATCHTOWER_SCOPE entirely (this updates every container, which is a wider blast radius).

If you don’t want auto-updates at all, just remove the watchtower service block.


10. Channel webhooks need a public HTTPS URL

Section titled “10. Channel webhooks need a public HTTPS URL”

LINE, WhatsApp, Feishu and the other webhook channels all need an HTTPS URL reachable from the public internet to receive messages. If your DuDuClaw instance runs on a home network or a server without a public IP, common options include:

Option Fits Cost Guide
Tailscale Funnel Home / dev / temporary Free §11
Cloudflare Tunnel Long-term production Free (requires your own domain) deployment-guide §4
ngrok Quick demos Free-tier URL changes each time deployment-guide §3
Reverse proxy (Caddy / Nginx) Own public IP + domain Requires ongoing maintenance deployment-guide §5

11. Tailscale Funnel (public HTTPS for webhooks)

Section titled “11. Tailscale Funnel (public HTTPS for webhooks)”

Compose ships with a Tailscale service, gated behind the tailscale profile by default so it doesn’t start automatically.

終端機視窗
# 1. Generate an auth key from the Tailscale admin console
# https://login.tailscale.com/admin/settings/keys
# We recommend checking "Reusable" + "Ephemeral"
# 2. Write it into .env
echo "TS_AUTHKEY=tskey-auth-xxxxxxx" >> .env
# 3. Start the tailscale profile
docker compose --profile tailscale up -d
# 4. Enable Funnel for this machine in the Tailscale admin console
# Machines → duduclaw → Enable Funnel
# 5. Get the https URL
docker compose exec tailscale tailscale funnel status
# https://duduclaw.your-tailnet.ts.net/ <- this is the webhook prefix

Set the LINE webhook URL to https://duduclaw.your-tailnet.ts.net/webhook/line.

The Tailscale container currently uses image: tailscale/tailscale:latest. For production, pin it to a specific digest instead (the compose file already has a TODO noting this).


終端機視窗
# Start / stop / restart
docker compose up -d
docker compose down # stops and removes the container (volumes stay)
docker compose restart duduclaw
# Rebuild the image (after code or Dockerfile changes)
docker compose build --no-cache duduclaw
docker compose up -d
# Enter a container shell
docker compose exec duduclaw bash
# Run a one-off command
docker compose exec duduclaw duduclaw agent list
docker compose exec duduclaw duduclaw doctor
# Logs
docker compose logs -f duduclaw
docker compose logs --since 1h duduclaw
# Cleanup
docker compose down -v # WARNING: removes volumes too — all data is lost
docker system prune -af # WARNING: removes every unused image / container

The container is up, but curl localhost:18789/health returns connection refused

Section titled “The container is up, but curl localhost:18789/health returns connection refused”

Cause: the gateway is bound to localhost inside the container, so the host can’t reach it. Check:

終端機視窗
docker compose exec duduclaw env | grep DUDUCLAW_BIND
# Must show DUDUCLAW_BIND=0.0.0.0

If it’s empty, add it under the compose file’s environment: block.

docker compose up gets stuck at Compiling duduclaw-gateway

Section titled “docker compose up gets stuck at Compiling duduclaw-gateway”

Rust compilation is CPU/RAM-heavy — the first build typically takes 10-20 minutes on a regular laptop. Later builds use the incremental cache and usually finish in 1-2 minutes.

If you’re low on memory (< 4 GB), the build can get OOM-killed. Options:

  • Use a pre-built image if one is available
  • Or run cargo build --release on the host ahead of time and rewrite the Dockerfile to COPY the binary directly (advanced)

Check in this order:

終端機視窗
# 1. Did the env var actually make it in?
docker compose exec duduclaw env | grep -E "CLAUDE_CODE_OAUTH_TOKEN|ANTHROPIC_API_KEY"
# 2. Is the volume mounted correctly?
docker compose exec duduclaw ls -la ~/.claude/
# 3. What does the CLI itself see?
docker compose exec duduclaw claude auth status

A common mistake is forgetting to restart the container with docker compose up -d after editing .env — environment variables are only injected at startup, so a hot edit has no effect.

Step 3 doesn’t prove the token still works. claude auth status reports loggedIn: true whenever a CLAUDE_CODE_OAUTH_TOKEN env var is present — it never checks whether that token still authenticates. A revoked or organization-disabled token passes this check just as happily as a good one. To find out whether the credential actually works, check that account’s credential-state badge on the dashboard’s Accounts page (未驗證/憑證損壞/token 無效/組織停用 — a healthy account shows nothing), or run a real probe inside the container:

終端機視窗
docker compose exec duduclaw claude -p "reply ok" --model haiku --max-turns 1 --output-format json

and look at the is_error / api_error_status fields in the JSON output.

The dashboard connects, but the agent replies with “please run claude auth status”

Section titled “The dashboard connects, but the agent replies with “please run claude auth status””

This is a leftover message from versions before v1.8.x; v1.8.22+ replaced it with zh-TW messages classified by FailureReason. If you’re still seeing it:

終端機視窗
docker compose exec duduclaw duduclaw --version
# Confirm it's 1.8.22 or later; if not, pull the latest main and rebuild

Symptom: the dashboard loads over HTTP through a tailnet (*.ts.net) or reverse-proxy domain, but live data spins forever; the browser DevTools Network panel shows the /ws upgrade returning 403.

Cause: the WebSocket Origin allowlist defaults to loopback only, and your external domain isn’t in it. Add it to the allowlist:

終端機視窗
# In compose's environment: block, or .env
DUDUCLAW_ALLOWED_ORIGINS=duduclaw.your-tailnet.ts.net,duduclaw.yourdomain.com

Or add it to ~/.duduclaw/config.toml:

[gateway]
allowed_origins = ["duduclaw.your-tailnet.ts.net"]

After a restart, the startup log prints the extra origins that took effect. See deployment-guide §5 WebSocket origin allowlist for details.

Channel webhook isn’t triggering the agent

Section titled “Channel webhook isn’t triggering the agent”
  • LINE / WhatsApp / Feishu need HTTPS — confirm §10 is set up
  • Check the connection status indicator for that channel under Dashboard → Channels
  • Check the logs: docker compose logs -f duduclaw | grep -i webhook

This only applies to agents with agent.toml [container] sandbox_enabled = true. Running the task sandbox from a gateway that itself runs in a container is not covered and has not been verified; the sandbox image must be present on the Docker daemon that the socket points to, and the sandbox is never pulled automatically (see task-sandbox.md). The container-side steps below only check that the socket works:

終端機視窗
# Test whether the socket works from inside the container
docker compose exec duduclaw docker ps
# You should see the container list from the host

If you get permission denied, it’s usually SELinux/AppArmor blocking it. On Linux, try:

volumes:
- /var/run/docker.sock:/var/run/docker.sock:rw
group_add:
- "${DOCKER_GID:-999}" # match the host's docker group GID

OAuth state disappears after a container rebuild

Section titled “OAuth state disappears after a container rebuild”

Confirm the volumes actually exist:

終端機視窗
docker volume ls | grep duduclaw
# duduclaw_duduclaw-data
# duduclaw_duduclaw-claude
# duduclaw_duduclaw-codex
# duduclaw_duduclaw-gemini

If one is missing, the compose file was changed or someone ran docker compose down -v at some point — just run claude auth login / codex login --device-auth again.