Here’s an unpopular opinion: running docker-setup.sh and calling it done teaches you nothing about what actually breaks in OpenClaw Docker deployments.
I spent two evenings debugging a “working” setup that mysteriously couldn’t connect the CLI to the gateway. Logs said the gateway was up. Port was open. Token matched. Still: ECONNREFUSED.
The problem? The CLI container wasn’t in the gateway’s network namespace. The setup script adds network_mode: "service:openclaw-gateway" automatically – but if you skip the script or customize anything, that line disappears. Nothing tells you why commands fail.
Why Manual Docker Config Beats the Setup Script
The official docker-setup.sh script handles onboarding, builds or pulls the image, writes your docker-compose.yml, and starts the gateway. Clean. Fast. But it hides three mismatches that silently break customized deployments:
- CLI network isolation – The CLI won’t reach
ws://127.0.0.1:18789unless it shares the gateway’s network namespace. - Token override trap – Setting
OPENCLAW_GATEWAY_TOKENin your.envfile silently overrides the token inopenclaw.json. Auth fails even when your config file is correct. - UID mismatch – The container runs as user
node(UID 1000). If your host directories aren’t owned by UID 1000, you get permission denied errors that Docker logs don’t explain clearly.
Building the compose file yourself forces you to confront these before they break at 2 AM.
Setup Script vs. Manual: What You Actually Trade
Use the script for throwaway testing. Build the compose file manually for production, home servers, or CI integration.
| Aspect | docker-setup.sh | Manual docker-compose.yml |
|---|---|---|
| Time to first run | 5 minutes | 15 minutes |
| Understanding | None – script does it all | You see every config decision |
| Customization | Edit afterward, risk breaking hidden dependencies | Choose defaults upfront |
| Debugging | Opaque – what did the script write? | You wrote it, you know what’s there |
| Updates | Re-run script, hope it merges cleanly | Edit compose file, control exactly what changes |
Manual Deployment Walkthrough
This assumes Docker and Docker Compose are installed. Get Docker here if you don’t have it.
Step 1: Create the Directory Structure
mkdir -p ~/.openclaw ~/openclaw/workspace
sudo chown -R 1000:1000 ~/.openclaw ~/openclaw/workspace
The container runs as UID 1000 (per the official Docker docs). Skip the chown? You’ll hit EACCES: permission denied when OpenClaw tries to write config files.
Step 2: Write docker-compose.yml
version: '3.8'
services:
openclaw-gateway:
image: ghcr.io/openclaw/openclaw:latest
container_name: openclaw-gateway
restart: unless-stopped
ports:
- "18789:18789"
volumes:
- ~/.openclaw:/home/node/.openclaw
- ~/openclaw/workspace:/home/node/.openclaw/workspace
environment:
- NODE_ENV=production
- TZ=UTC
command: [
"node",
"dist/index.js",
"gateway",
"--bind", "lan",
"--port", "18789"
]
openclaw-cli:
image: ghcr.io/openclaw/openclaw:latest
network_mode: "service:openclaw-gateway"
volumes:
- ~/.openclaw:/home/node/.openclaw
- ~/openclaw/workspace:/home/node/.openclaw/workspace
profiles:
- tools
Three lines that matter:
network_mode: "service:openclaw-gateway" on the CLI. Without it? The CLI tries ws://127.0.0.1:18789 and gets ECONNREFUSED because it’s in a separate network namespace.
--bind lan lets host browsers reach the gateway. Don’t use --bind 0.0.0.0. The CLI rejects it – as of version 2026.3.8, confirmed in GitHub Issue #44101.
profiles: [tools] on CLI prevents the CLI container from starting automatically. You run it on-demand with docker compose run.
Step 3: Start the Gateway
docker compose up -d openclaw-gateway
docker compose logs -f openclaw-gateway
Wait for [gateway] listening on ws://0.0.0.0:18789 in the logs.
Step 4: Run Onboarding
docker compose run --rm openclaw-gateway
node dist/index.js onboard --mode local --no-install-daemon
This runs interactively. It’ll ask for model provider (Anthropic, OpenAI, OpenRouter). As of April 4, 2026, Anthropic shut down OAuth for third-party tools – you’ll need an API key, not subscription credits. Paste your API key. It saves to ~/.openclaw/openclaw.json. The gateway token auto-generates. That’s what you’ll use to auth the dashboard.
Pro tip: OpenAI with OAuth? The wizard opens a browser URL. In headless Docker, the browser won’t launch – copy the redirect URL manually and paste it back into the terminal (official Docker guide covers this).
Step 5: Access the Dashboard
docker compose run --rm --profile tools openclaw-cli dashboard --no-open
Copy the URL it prints (something like http://127.0.0.1:18789/?token=abc123...). Open it in your browser. You’re in.
The Three Gotchas That Break Silently
1. CLI Can’t Reach Gateway (ECONNREFUSED)
Symptom: docker compose run openclaw-cli devices list fails with connect ECONNREFUSED 127.0.0.1:18789.
The CLI defaults to ws://127.0.0.1:18789. But unless you add network_mode: "service:openclaw-gateway", it’s in a separate network namespace where 127.0.0.1 means the CLI container’s own loopback – not the gateway’s.
Fix: Add that line to docker-compose.yml under openclaw-cli. (Issue #5559 documents this – the manual Docker setup instructions originally omitted it.)
2. Token Mismatch Even When Config Looks Right
Dashboard shows “unauthorized: gateway token mismatch.” You check ~/.openclaw/openclaw.json – the token matches what you copied. Still fails.
Set OPENCLAW_GATEWAY_TOKEN in your .env file or as an environment variable in docker-compose.yml? It silently overrides gateway.auth.token from the config file. The gateway uses the env var, the CLI uses the config file – mismatch.
Fix: Remove OPENCLAW_GATEWAY_TOKEN from your env entirely, or make sure it matches the token in openclaw.json. (Issue #9028 documents this.)
3. Permission Denied on /home/node/.openclaw
Symptom: Error: EACCES: permission denied, mkdir '/home/node/.openclaw/identity'
The container runs as user node (UID 1000). Your host directories are probably owned by your user (UID 1001, 501, etc.). UID mismatch – container can’t write.
Fix:
sudo chown -R 1000:1000 ~/.openclaw ~/openclaw/workspace
Do this before first run. Already started the container? Stop it, fix ownership, restart. This is the #1 issue in GitHub reports (#5434, #23948).
Connecting a Messaging Channel
Telegram is the easiest to test. Create a bot via @BotFather, get the token, then:
docker compose run --rm --profile tools openclaw-cli
channels add --channel telegram --token "YOUR_BOT_TOKEN"
OpenClaw sends a pairing code to your Telegram. Approve it:
docker compose run --rm --profile tools openclaw-cli
pairing approve telegram YOUR_CODE
Now message your bot. It replies via the gateway.
WhatsApp, Slack, Discord – same pattern. Add the channel, approve pairing. The official channel docs cover each platform.
What About Local Models?
Zero API costs? Docker released Docker Model Runner integration in February 2026. Pull a model with docker model pull, point OpenClaw at localhost:12434, done. Fully local. No telemetry.
The catch: these models eat RAM. Docker’s blog notes that “over a quarter of all production code is now AI-authored” (as of March 2026) – but that doesn’t mean your 2GB VPS can run a 7B model smoothly. Budget 8GB+ for anything serious.
Your Next Move
You’ve got a working gateway. CLI connects. Dashboard is up. Telegram bot responds.
Install a skill. The dashboard has a Skills panel – search for “calendar,” “github,” “notion,” whatever you use. Click install. Your agent can now interact with those services. Or connect a second channel. Or set up a cron job to summarize your inbox every morning.
The compose file you built gives you full control to add, remove, or tweak any part of the stack without re-running a script that might overwrite your changes. That’s the real win: you understand what’s running, so you can fix it when it breaks.
FAQ
Can I use docker run instead of docker compose?
Yes, but you lose the CLI container’s network_mode trick. Stick with Compose.
My gateway keeps restarting with exit code 137 – what’s wrong?
Exit 137 is an OOM kill. The Linux kernel terminated the process because it ran out of memory. Building the image locally (not using the pre-built ghcr.io/openclaw/openclaw:latest)? pnpm install needs at least 2 GB RAM. On a 1 GB VPS, it’ll silently fail. I ran into this on a DigitalOcean droplet – upgraded to 2GB, worked fine. Use the pre-built image, or upgrade your VPS (per the official docs and confirmed by the QubitTool deployment guide).
How do I update OpenClaw to the latest version?
docker compose pull openclaw-gateway
docker compose up -d openclaw-gateway
docker image prune -f
Your config and workspace are in mounted volumes, so they survive. The prune cleans up the old image. Latest version as of this writing is 2026.4.11, released April 2026.