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
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.
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.
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.
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.
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.
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:
終端機視窗
curlhttp://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)
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):
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
npminstall-g@anthropic-ai/claude-code
claudesetup-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:
終端機視窗
dockercomposeup-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.
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.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.
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:
# 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
dockercompose--profiletailscaleup-d
# 4. Enable Funnel for this machine in the Tailscale admin console
# Machines → duduclaw → Enable Funnel
# 5. Get the https URL
dockercomposeexectailscaletailscalefunnelstatus
# 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).
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)
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:
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:
終端機視窗
dockercomposeexecduduclawduduclaw--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:
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.