Skip to content

Docker installation guide

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

This guide covers the full process of deploying the DuDuClaw server with Docker Compose — port configuration, authentication for all three CLIs (Claude / Codex / Gemini), data persistence, channel webhooks, and auto-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, @google/gemini-cli (installed via npm i -g)
  • The docker.io CLI (used to call the host Docker daemon to create agent sandboxes)
  • (Optional) Python 3.12: only needed for advanced local inference (MLX / LLMLingua-2, requires mlx_lm / 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, generic webhook) 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, recommended, fits browser-less container environments
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 Authenticating the three CLIs 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.


Each of the three runtimes supports two authentication paths: OAuth (no extra cost, runs on your subscription plan) or an API key (usage-billed fallback). In practice, set up OAuth first and keep an API key as a backup.

Each of the three OAuth state directories inside the container has its own named volume:

Path Volume CLI
/home/duduclaw/.claude duduclaw-claude Claude Code
/home/duduclaw/.codex duduclaw-codex Codex
/home/duduclaw/.gemini duduclaw-gemini Gemini

This means you only need to log in once — rebuilding the container doesn’t require logging in again.

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
# Enter your ChatGPT credentials or follow the browser flow
# 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.

.env:

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

Billed by usage, at OpenAI API pricing.

終端機視窗
docker compose exec duduclaw bash
gemini auth
# Follow the prompts to complete Google login in your local browser
# State is written to /home/duduclaw/.gemini/, i.e. the duduclaw-gemini volume
exit

Google currently offers a free quota for the Gemini CLI, which is fine for everyday use.

.env:

終端機視窗
GEMINI_API_KEY=AIza...

Billed at Google AI Studio pricing.

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

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

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

If nothing is specified, RuntimeRegistry scans PATH on startup and picks the first available runtime.

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

docker-compose.yml declares four 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 Gemini OAuth state Moderate

Plus a mounted host path:

Path Purpose Requirement
/var/run/docker.sock Lets the container call the host Docker daemon to create agent sandboxes Required, used for container-sandbox isolated execution
終端機視窗
# 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
- /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}
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 generic webhooks 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 cost summary
# 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

Container sandbox can’t start a sub-agent

Section titled “Container sandbox can’t start a sub-agent”

DuDuClaw calls the host Docker daemon through docker.sock to create isolated containers for running agent tasks. If this fails:

終端機視窗
# 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 / gemini auth again.