Deployment Guide
How to run Clawback as a public self-hosted single-node deployment.
Audience: Operators deploying Clawback on a VM or single host.
Supported Setup
The supported deployment today is:
- self-hosted
- single node
- Docker Compose first
- console exposed publicly
- control plane kept internal when possible
Clawback is not currently a multi-node or HA product.
What Ships
The production packaging currently lives in:
docker-compose.prod.yml.env.prod.exampleservices/control-plane/Dockerfileservices/runtime-worker/Dockerfileapps/console/Dockerfileinfra/caddy/Caddyfile
The stack includes:
postgresminioopenclawmigratecontrol-planeruntime-workerconsolecaddy(TLS reverse proxy)
1. Prepare Environment Variables
Copy the production example file:
cp .env.prod.example .env
Or generate a ready-to-edit production env file with strong random secrets:
pnpm generate:prod-env -- --domain demo.clawback.team --output .env
At minimum, set strong values for:
POSTGRES_PASSWORDMINIO_ROOT_PASSWORDOPENCLAW_GATEWAY_TOKEN- one model provider key, usually
OPENAI_API_KEY COOKIE_SECRETCLAWBACK_RUNTIME_API_TOKENCLAWBACK_APPROVAL_SURFACE_SECRETCONSOLE_ORIGINCLAWBACK_DOMAIN
For the provided Compose file, keep:
CONTROL_PLANE_INTERNAL_URL=http://control-plane:3001
That is what allows the console container to proxy browser and webhook traffic to the control plane.
2. Optional Provider Secrets
Only set these if you are using the related provider:
CLAWBACK_INBOUND_EMAIL_WEBHOOK_TOKENCLAWBACK_GMAIL_WATCH_HOOK_TOKENCLAWBACK_SMTP_*SLACK_BOT_TOKENSLACK_SIGNING_SECRETWHATSAPP_*
Important product truth:
- Gmail is optional
- SMTP is optional until you want real reviewed email delivery
- forward-email and local retrieval remain valid first-value paths without Gmail
Operator note:
CLAWBACK_INBOUND_EMAIL_WEBHOOK_TOKENis optional for the product overall, but required if you want the deployed forward-email webhook path to work or if you want to run./scripts/public-try-verify.shagainst the deployed stack
3. Build and Start
docker compose -f docker-compose.prod.yml --env-file .env up -d --build
The migrate service runs database migrations before the control plane starts.
4. Verify Container Health
Check container status:
docker compose -f docker-compose.prod.yml ps
The following services define Dockerfile-level healthchecks and will report healthy or unhealthy in the status column:
postgres—pg_isreadyopenclaw—node dist/index.js healthcontrol-plane— HTTP probe against/healthzon port 3001runtime-worker— runsservices/runtime-worker/dist/healthcheck.js
The console service depends on control-plane being healthy. The control-plane and runtime-worker services depend on postgres and openclaw being healthy, and on migrate completing successfully.
Check logs if anything looks wrong:
docker compose -f docker-compose.prod.yml logs -f control-plane console runtime-worker
If you are still deciding between the shared demo, local quickstart, and this deployment path, read Start Here first.
Fresh VM Rehearsal
If you want to rehearse the current single-node deployment path on a fresh Ubuntu/Debian VM before doing a real rollout, use the remote rehearsal script from your local checkout:
./scripts/test-remote-stack.sh --host root@<vm-ip>
If you are using Hetzner Cloud specifically, you can also provision the rehearsal VM from your local machine first:
HCLOUD_TOKEN=... ./scripts/provision-hetzner-rehearsal.sh
Before a real Hetzner + TLS rollout, you can also run a local preflight to see exactly what is still missing:
pnpm check:hetzner-deploy
This bootstraps Docker on the remote host, syncs the current repo snapshot, and runs the existing deployed-stack acceptance flow there.
What you learn from this rehearsal:
- the host can be prepared for the supported Compose deployment
- the production stack builds and reaches health on a fresh VM
- seeding and the no-Google public-try path still pass remotely
What it still does not cover:
- TLS / reverse proxy (Caddy is in the compose file but needs a real domain)
- SMTP-backed reviewed-send delivery
- Gmail-connected acceptance
- persistent deployment with a retained
.env
Updating An Existing Remote Host
If you already have a VM running the supported production Compose stack and just want to push the current checkout onto it, use:
./scripts/deploy-remote-stack.sh --host user@host
Common options:
./scripts/deploy-remote-stack.sh \
--host user@host \
--identity ~/.ssh/id_ed25519 \
--workspace ~/clawback-deploy \
--env-file .env
This syncs the current repo snapshot, preserves the remote .env, and runs:
docker compose -f docker-compose.prod.yml --env-file .env up -d --build
Use --skip-rsync if the remote workspace is already current and you only want
to restart, or --no-build if you explicitly want to reuse the images already
present on the host.
If you also deploy clawback.team
The public site links visitors into the docs served by the demo/console
deployment at https://demo.clawback.team/docs/*.
Deploy in this order:
- deploy the demo/console/docs surface
- confirm the live docs match this checkout
- deploy the site
From your local checkout, run:
pnpm check:demo-docs-sync
This compares the local public-docs hash against the live demo endpoint at
https://demo.clawback.team/api/docs/version.
If it fails, redeploy the demo/console surface first. Do not deploy the site until the check passes.
5. Verify the Control Plane
From the host or a trusted internal network path:
curl -s http://127.0.0.1:3001/healthz
curl -s http://127.0.0.1:3001/readyz
Expected:
/healthzreturns200/readyzreturns200once Postgres and PgBoss are ready
6. TLS and Reverse Proxy
The production compose file includes a Caddy reverse proxy that terminates TLS automatically via Let's Encrypt. This is the recommended path for single-node deployments.
Why Caddy
Caddy was chosen over nginx for this deployment shape because:
- Automatic ACME certificate provisioning and renewal with zero extra config
- No certbot sidecar, no cron jobs, no manual cert-path plumbing
- Minimal config surface (~10 lines vs ~40 for nginx + certbot)
- HTTP-to-HTTPS redirect is automatic
The only requirement is that the host's DNS A record points to the VM and ports 80/443 are reachable from the internet.
Architecture
Internet -> :443 (Caddy, TLS) -> console:3000 -> control-plane:3001
(internal Docker network)
- Caddy is the only service bound to public ports (80 and 443)
- The console and control-plane ports are bound to
127.0.0.1only - The console already proxies
/api/*to the control plane internally viaCONTROL_PLANE_INTERNAL_URL, so Caddy only needs to reach the console - SSE streams (
/api/runs/*/stream) are handled with unbuffered flushing
Setup
- Set
CLAWBACK_DOMAINin your.envto the public hostname:
CLAWBACK_DOMAIN=clawback.example.com
CONSOLE_ORIGIN=https://clawback.example.com
- Ensure DNS is pointing to the host:
dig +short clawback.example.com # should return the VM's public IP
-
Ensure ports 80 and 443 are open in your firewall / security group.
-
Start the stack:
docker compose -f docker-compose.prod.yml --env-file .env up -d --build
Caddy will automatically obtain a TLS certificate on first request. You can watch the ACME handshake:
docker compose -f docker-compose.prod.yml logs -f caddy
Verifying TLS
curl -I https://clawback.example.com
Expected: HTTP 200 with a valid TLS certificate.
Certificate Persistence
Certificates are stored in the caddy-data Docker volume. As long as this
volume is retained across restarts, Caddy will not re-request certificates.
Skipping Caddy
If you are placing Clawback behind an existing load balancer or CDN that already
terminates TLS, you can remove the caddy service from the compose file and
change the console port binding back to "${CONSOLE_PORT:-3000}:3000" (removing
the 127.0.0.1 prefix).
Custom Caddyfile
The Caddyfile lives at infra/caddy/Caddyfile and is mounted read-only. To
customize (e.g. add rate limiting, custom headers, or additional domains), edit
that file and restart the caddy service:
docker compose -f docker-compose.prod.yml restart caddy
7. Bootstrap and Verify
On a fresh database:
- open
https://clawback.example.com/setup - create the first admin
- log in
- optionally seed demo data if this is an evaluation environment
- run the public verification flow
For a real smoke verification:
pnpm smoke:public-try
If you are running from a built host rather than a dev shell, invoke the script directly against the deployed URL:
CONTROL_PLANE_URL=https://clawback.example.com ./scripts/public-try-verify.sh
For the full no-Google verification path, also export the inbound webhook token used by your deployment:
CONTROL_PLANE_URL=https://clawback.example.com \
CLAWBACK_INBOUND_EMAIL_WEBHOOK_TOKEN=... \
./scripts/public-try-verify.sh
Current verifier behavior on a no-SMTP deployment:
- watched inbox is skipped if Gmail is not connected
- review approval is skipped if the pending review is
send_emailand SMTP is not connected - denial still runs, so the review-resolution path is exercised even on the no-SMTP public-try story
8. Provider-Specific Notes
Gmail
Gmail setup happens in-product from /workspace/connections.
What is required:
- an operator-supplied Google OAuth app or service account
- attaching the Gmail connection to the right worker
- running
Check inbox nowto establish or advance monitoring
SMTP
SMTP requires server-side environment variables before the relay can be marked connected from the UI.
Webhooks
Webhook-style integrations can target the public console origin under /api/..., because the console proxies those requests through to the control plane.
Examples:
/api/inbound/email/postmark/api/inbound/gmail-watch/.../api/webhooks/n8n/...
9. Backups and Recovery
Minimum operational stance:
- back up Postgres
- persist MinIO data if artifacts matter for your deployment
- keep the
.envfile and secrets recoverable
Clawback does not provide automatic backup orchestration yet.
10. Known Limits of This Deployment Shape
Current limits of this deployment shape:
- single-node only
- no HA or clustering
- no built-in metrics stack
- no built-in managed secret store
- no published container registry images yet
Read Known Limitations before making broader production promises.