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
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.
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; 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.
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):
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
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.
# 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).
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.
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
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 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:
# 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:
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
dockercomposeexecduduclawdockerps
# 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
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.