DuDuClaw deployment guide
Updated: 2026-03-30 | Version: v0.10.0
1. Local development
Section titled “1. Local development”# Buildcargo build --release
# Run (starts gateway + channels + heartbeat + cron + dispatcher)duduclaw run
# Access Dashboardopen http://localhost:18789Default port: 18789. Configure in ~/.duduclaw/config.toml:
[gateway]bind = "127.0.0.1"port = 18789Health check
Section titled “Health check”curl http://localhost:18789/health# {"status":"ok","version":"0.10.0","uptime_seconds":42,"agents_loaded":2,"channels_connected":["telegram","discord"]}
curl http://localhost:18789/health/ready # 200 when agents loadedcurl http://localhost:18789/health/live # 200 always (liveness probe)2. Tailscale Funnel (recommended for LINE webhook)
Section titled “2. Tailscale Funnel (recommended for LINE webhook)”LINE Messaging API requires a public HTTPS URL for webhooks. Tailscale Funnel provides this without a VPS, static IP, or domain.
# 1. Install Tailscalebrew install tailscale # macOScurl -fsSL https://tailscale.com/install.sh | sh # Linux
# 2. Authenticatetailscale up
# 3. Enable HTTPS + Funneltailscale funnel 18789
# This gives you a URL like:# https://your-machine.tail12345.ts.net/Configure LINE
Section titled “Configure LINE”- Go to LINE Developers Console
- Select your Messaging API channel
- Set Webhook URL to:
https://your-machine.tail12345.ts.net/webhook/line - Enable “Use webhook”
- Verify by clicking “Verify” button
Persistent funnel
Section titled “Persistent funnel”# Run as background servicetailscale funnel --bg 18789
# Or via systemd (Linux)# Add to duduclaw.service After=tailscaled.service3. ngrok (alternative)
Section titled “3. ngrok (alternative)”# 1. Installbrew install ngrok # macOSsnap install ngrok # Linux
# 2. Authenticate (free account)ngrok config add-authtoken YOUR_TOKEN
# 3. Start tunnelngrok http 18789
# Copy the HTTPS URL (e.g., https://abc123.ngrok-free.app)# Set as LINE Webhook URL: https://abc123.ngrok-free.app/webhook/lineNote: Free ngrok URLs change on restart. Use ngrok http 18789 --domain=your-domain.ngrok-free.app with a reserved domain.
4. Cloudflare Tunnel (long-term stable)
Section titled “4. Cloudflare Tunnel (long-term stable)”Best for production — free, stable URL, no port forwarding.
# 1. Install cloudflaredbrew install cloudflared # macOS
# 2. Logincloudflared tunnel login
# 3. Create tunnelcloudflared tunnel create duduclaw
# 4. Configure (in ~/.cloudflared/config.yml)cat > ~/.cloudflared/config.yml << 'EOF'tunnel: YOUR_TUNNEL_IDcredentials-file: /Users/YOU/.cloudflared/YOUR_TUNNEL_ID.json
ingress: - hostname: duduclaw.yourdomain.com service: http://localhost:18789 - service: http_status:404EOF
# 5. Add DNS recordcloudflared tunnel route dns duduclaw duduclaw.yourdomain.com
# 6. Runcloudflared tunnel run duduclawSet LINE Webhook: https://duduclaw.yourdomain.com/webhook/line
5. Reverse proxy (Caddy / Nginx)
Section titled “5. Reverse proxy (Caddy / Nginx)”Caddy (auto TLS)
Section titled “Caddy (auto TLS)”duduclaw.yourdomain.com { reverse_proxy localhost:18789
# WebSocket support (auto-detected by Caddy) # No extra config needed}caddy run --config Caddyfileserver { listen 443 ssl; server_name duduclaw.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/duduclaw.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/duduclaw.yourdomain.com/privkey.pem;
location / { proxy_pass http://127.0.0.1:18789; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 86400; }}WebSocket origin allowlist (required reading for reverse proxy / tailnet setups)
Section titled “WebSocket origin allowlist (required reading for reverse proxy / tailnet setups)”By default, the dashboard’s live connections (WebSocket, WebChat) only accept
a browser Origin from loopback (localhost / 127.0.0.1 / [::1]). When you
open the dashboard through a reverse proxy domain or a Tailscale/tailnet
address, the HTTP page loads fine, but the WebSocket upgrade gets rejected
with 403 and the screen spins forever. Add the external domain to the
allowlist to fix it:
[gateway]# host, host:port, or a full origin with scheme all work (normalized on load)allowed_origins = ["duduclaw.yourdomain.com", "box.your-tailnet.ts.net"]Or via an environment variable (comma-separated, merged with the config.toml list rather than replacing it):
DUDUCLAW_ALLOWED_ORIGINS="duduclaw.yourdomain.com,box.your-tailnet.ts.net"- The three built-in loopback entries are always allowed and don’t need to be listed; an empty list behaves exactly like older versions.
- Each entry is an exact host or host:port match — no wildcards. A
port-less entry matches that host on any port. Suffix attacks
(
duduclaw.yourdomain.com.evil.com) are rejected. - On startup the gateway logs one info line with the active extra origins, for easier troubleshooting.
- You can also add/remove entries directly from the dashboard under Settings → System → Remote access URLs, without touching config.toml — changes take effect immediately on save, no gateway restart needed (entries from the environment variable are preserved either way).
Dashboard deep links in channel push messages ([dashboard] public_url)
Section titled “Dashboard deep links in channel push messages ([dashboard] public_url)”When an AI employee pushes a “please handle this in the dashboard” message over LINE, Telegram, Slack, and similar channels, it attaches a clickable link that goes straight to that task’s or approval’s detail page (not the home page). Here’s how that link gets built:
- It first reads
[dashboard] public_urlfromconfig.toml(your external domain — for example, the one exposed through a reverse proxy or tailnet); - If that isn’t set, it falls back to
http://localhost:<[gateway] port>, which only actually opens when the user is on the same machine as the gateway; - If neither is available, no link is attached — the message text stays as is (no empty link appears).
When you expose the dashboard externally through a reverse proxy or tailnet,
set public_url:
[dashboard]public_url = "https://duduclaw.yourdomain.com"In-Telegram approval detail card ([miniapp] enabled, experimental, off by default)
Section titled “In-Telegram approval detail card ([miniapp] enabled, experimental, off by default)”When public_url is https, you can turn on an experimental feature: the
Telegram card for approving a high-risk action gains a “🔎 View details”
button that expands the full explanation, a simulated outcome, and an
expiry countdown right inside the chat — approve or reject on the spot,
no browser switch needed.
[miniapp]enabled = trueWhen public_url isn’t https, or when the card is sent to a group (Telegram
only allows this kind of button in private chats), the button is omitted and
the card is identical to having the feature off. Full details and the
security model are in
docs/features/43-telegram-miniapp.md.
6. Docker Compose
Section titled “6. Docker Compose”→ Full guide: docs/guides/docker.md — covers the three CLI auth setups, port details, volume backups, watchtower, and troubleshooting.
cd /path/to/DuDuClawdocker compose up -dservices: gateway: build: context: . dockerfile: container/Dockerfile.server ports: - "18789:18789" volumes: - ~/.duduclaw:/home/duduclaw/.duduclaw environment: - DUDUCLAW_HOME=/home/duduclaw/.duduclaw env_file: - .env restart: unless-stopped healthcheck: test: ["CMD", "curl", "-f", "http://localhost:18789/health"] interval: 30s timeout: 10s retries: 3 start_period: 10s.env file:
# Required for channel bots (encrypted at rest via duduclaw onboard)# ANTHROPIC_API_KEY=sk-ant-... # Only if not using encrypted config7. System service (launchd / systemd)
Section titled “7. System service (launchd / systemd)”# Install as system service (auto-detects OS)duduclaw service install
# Managementduduclaw service startduduclaw service stopduduclaw service statusduduclaw service logs --lines 50duduclaw service uninstallinstall / uninstall register (or remove) a user-level autostart entry —
no sudo required — and never touch a running gateway; the change takes effect
at the next login. The same registration can be toggled from the dashboard
(Settings → General → Start at login), which the onboarding wizard also offers
on its final step.
macOS (launchd)
Section titled “macOS (launchd)”Creates ~/Library/LaunchAgents/com.duduclaw.gateway.plist
Linux (systemd)
Section titled “Linux (systemd)”Creates ~/.config/systemd/user/duduclaw.service and enables it via the
default.target.wants symlink (equivalent to systemctl --user enable duduclaw).
Windows
Section titled “Windows”Creates the DuDuClaw value under
HKCU\Software\Microsoft\Windows\CurrentVersion\Run.
8. Auto-update
Section titled “8. Auto-update”The gateway checks GitHub Releases every 6 hours and the dashboard (Settings → Update) has a manual Check / Install flow. Both paths share the same pipeline:
- Download the platform asset (
duduclaw-<platform>.tar.gz/.zip) - Verify the SHA-256 sidecar and the minisign Ed25519 signature
(
<asset>.minisig, public key pinned in the binary — unsigned or tampered releases are rejected, no override) - Verify the new binary executes (
duduclaw version), then atomically swap it in place (backup + rename, auto-rollback on failure) - Graceful shutdown, then re-exec the new binary in-process — the PID
is preserved on macOS/Linux, so launchd/systemd keep supervising, and
unsupervised foreground runs (npm wrapper,
duduclaw run) restart too - Open dashboard tabs show a restart banner and reload automatically
Enable unattended updates:
[gateway]auto_update = true # default: false — dashboard notification onlyOr DUDUCLAW_AUTO_UPDATE=1 (env wins over config).
Notes by install method:
| Install method | Behavior |
|---|---|
| Standalone / npm | Self-update in place (npm registry metadata goes stale until the next npm i -g duduclaw, harmless) |
| Homebrew (discontinued) | Self-update refuses; the tap is retired and will never receive new versions — reinstall via npm or the desktop app instead |
Source (cargo/target/) |
Self-update allowed but a rebuild will overwrite |
9. Prometheus + Grafana monitoring
Section titled “9. Prometheus + Grafana monitoring”Prometheus scrape config
Section titled “Prometheus scrape config”scrape_configs: - job_name: 'duduclaw' static_configs: - targets: ['localhost:18789'] metrics_path: '/metrics' scrape_interval: 30sAvailable metrics (v0.12.0+)
Section titled “Available metrics (v0.12.0+)”| Metric | Type | Description |
|---|---|---|
duduclaw_requests_total |
Counter | Total requests by agent, channel, runtime, status |
duduclaw_tokens_total |
Counter | Total tokens by agent, type (input/output/cache_read) |
duduclaw_request_duration_seconds |
Histogram | Request latency by agent, runtime |
duduclaw_active_sessions |
Gauge | Currently active sessions |
duduclaw_channel_connected |
Gauge | Channel connection status (1/0) |
duduclaw_failover_total |
Counter | Provider failover events |
duduclaw_budget_remaining_cents |
Gauge | Remaining budget per account |
Grafana dashboard
Section titled “Grafana dashboard”Import the following JSON into Grafana (Dashboards > Import):
{ "dashboard": { "title": "DuDuClaw", "panels": [ {"title": "Requests/min", "type": "stat", "targets": [{"expr": "rate(duduclaw_requests_total[5m])*60"}]}, {"title": "Token Usage", "type": "timeseries", "targets": [{"expr": "rate(duduclaw_tokens_total[5m])*60"}]}, {"title": "Response Time p95", "type": "stat", "targets": [{"expr": "histogram_quantile(0.95, rate(duduclaw_request_duration_seconds_bucket[5m]))"}]}, {"title": "Channels", "type": "table", "targets": [{"expr": "duduclaw_channel_connected"}]}, {"title": "Budget", "type": "bargauge", "targets": [{"expr": "duduclaw_budget_remaining_cents"}]} ] }}Monitoring quick start
Section titled “Monitoring quick start”# docker-compose with monitoringdocker compose -f docker-compose.yml -f docker-compose.monitoring.yml up -d10. Enterprise LAN deployment (employee desktops → company gateway)
Section titled “10. Enterprise LAN deployment (employee desktops → company gateway)”A common enterprise setup: run one gateway on a shared server and have each employee’s desktop app connect to it over the office network. The desktop app ships a Gateway picker (shown before login) that finds gateways on the LAN automatically via mDNS, so employees never type an IP.
Server side — advertise on the LAN
Section titled “Server side — advertise on the LAN”The gateway advertises itself over mDNS/DNS-SD as _duduclaw._tcp.local..
Advertising is off by default (opt-in) — only a gateway you deliberately
mark as an “office gateway” appears on the LAN, so employee desktops and stray
instances never turn into discoverable gateways. On the shared server, opt in and
bind to a LAN interface (not loopback):
# ~/.duduclaw/config.toml on the gateway server[gateway]bind = "0.0.0.0" # reachable on the LAN (default 127.0.0.1 = local only)port = 18789
[general]name = "Office Gateway" # shown as the instance name in the desktop picker
[server]mdns_advertise = true # default FALSE; set true to broadcast on the LANtls = false # set true when the gateway is fronted by HTTPS (below)You can flip these in the dashboard under Settings → System → Server (admin only) instead of editing the file — the display name, bind interface, and mDNS switch are all editable there (bind/broadcast changes note that a gateway restart is required).
- With
mdns_advertise = false(the default), the gateway never broadcasts; employees connect by typinghost:portmanually in the picker. - Env override:
DUDUCLAW_MDNS_ADVERTISE=0|1takes precedence over config. The desktop app’s own bundled sidecar sets=0, so a laptop running the desktop app is never advertised on the network regardless of itsconfig.toml. - The advertisement carries the gateway version, the display name, and a
tlsflag in its TXT record; no credentials or secrets are broadcast. - Advertising is best-effort: if mDNS registration fails (locked-down network, no multicast), the gateway logs a warning and serves normally.
- On graceful shutdown the gateway withdraws the advertisement (mDNS goodbye), so employees stop seeing a gateway that has gone away.
HTTPS recommendation
Section titled “HTTPS recommendation”mDNS discovery yields a plain http://<ip>:<port> endpoint, which is fine on a
trusted internal network. For anything crossing untrusted segments — or as a
default hardening step — front the gateway with a reverse proxy that terminates
TLS (see §5 Caddy/Nginx) and set [server] tls = true so the picker shows the
endpoint as HTTPS. Employees can also type the HTTPS proxy hostname manually.
Remember to add the proxy hostname to [gateway] allowed_origins (see §5’s
WebSocket Origin allowlist) or the dashboard WS upgrade will be rejected.
Employee side — the desktop Gateway picker
Section titled “Employee side — the desktop Gateway picker”On launch the desktop app auto-selects a gateway and connects without asking, falling back to a picker only when it can’t decide:
- If it remembers a gateway and that gateway’s
/healthzresponds → connect to it straight away (no picker, no countdown). - Otherwise it scans the LAN: exactly one gateway found → connect to it and show a brief toast; several found → show the picker list; none found → start and connect to the local bundled gateway.
- If the remembered gateway is unreachable, it falls to the picker so the employee can choose.
The picker itself offers three ways to connect:
- 本機 / Local — the app’s own bundled gateway (for solo use).
- 區網偵測 / On your network — gateways discovered via mDNS, with a rescan
button. Each row shows name,
host:port, and version. - 手動輸入 / Manual — type
192.168.1.10:18789orhttps://gw.company.com. The address is validated against/healthzbefore connecting; a bad address shows an error and does not navigate.
To switch gateways later, use 切換 Gateway / Switch Gateway in the app’s tray
menu — it reopens the picker. Choosing a remote gateway releases the local
sidecar (no competing local instance is left running). Only http/https
addresses are accepted (fail-closed).
Discovery only advertises a display name — it is not an authentication boundary. Login and authorization are always enforced by the target gateway, so a spoofed advertisement can at most show a misleading name, never bypass auth.
Quick reference
Section titled “Quick reference”| Method | URL | Use Case |
|---|---|---|
| Local only | http://localhost:18789 |
Development |
| Enterprise LAN | mDNS auto-discovery (desktop picker) | Employees → company gateway |
| Tailscale | https://xxx.ts.net |
Home server, LINE webhook |
| ngrok | https://xxx.ngrok-free.app |
Quick demo |
| Cloudflare | https://duduclaw.yourdomain.com |
Production |
| Docker | docker compose up -d |
Server deployment |
| Service | duduclaw service install |
Auto-start on boot |