OpenClaw runs well in Docker: pull the official ghcr.io/openclaw/openclaw image, bind-mount a host directory owned by uid 1000 to /home/node/.openclaw, keep the API keys and the gateway token in a .env file, publish port 18789 on 127.0.0.1 only and start it with docker compose up -d. On a small Linux virtual machine the install takes about ten minutes; the sandbox caveat at the end is the part most Docker users miss.
What the Docker image gives you
OpenClaw is an open-source (MIT, OpenClaw Foundation) personal AI agent that runs as a long-lived gateway: one Node.js process that holds the channel connections, talks to the LLM provider and executes tools. The native install needs Node 24.16+ or 26.1+; the image carries its own runtime, so the host needs only Docker Engine and Compose v2.
The image lives on GitHub Container Registry as ghcr.io/openclaw/openclaw, mirrored on Docker Hub as openclaw/openclaw. Tags follow the release calendar (for example 2026.9.3), plus the moving channels latest, main and extended-stable, and variants such as -slim and -browser. Plain version tags and dated -rYYYYMMDD tags are immutable, and the docs recommend pinning one so a deployment does not follow a moving tag. The newest release at the time of writing is v2026.9.4 (11 September 2026).
Inside the image the process runs as the unprivileged node user, uid 1000; the Dockerfile pre-creates /home/node/.openclaw with mode 0700 owned by that user, which is why host ownership matters in step 1. The docs ask for 6 GB RAM only when building from source; with the pre-built image, in our deployments the gateway idles at a few hundred megabytes, so 1 to 2 GB is enough for a Telegram-only agent.
Step 1: prepare the state directory
Everything that must survive a restart lives in one directory: openclaw.json, the workspace, the SQLite state files and channel state. The docs bind-mount it so it survives container replacement, and the troubleshooting page gives the same fix for permission errors: the host directory must be owned by uid 1000, the user the container runs as.
mkdir -p ~/.openclaw/workspace ~/.openclaw-auth-profile-secrets
sudo chown -R 1000:1000 ~/.openclaw ~/.openclaw-auth-profile-secrets
chmod 700 ~/.openclaw
On a fresh Ubuntu VM the first login user is usually uid 1000 already; when you work as root, the chown is what prevents EACCES errors in the logs.
Step 2: the Compose file
The official route is to clone the repository and run ./scripts/docker/setup.sh, which builds or pulls the image, writes .env and starts onboarding. On a server a hand-written Compose file with a pinned tag and a loopback-only publish is easier to audit. The file below is trimmed from the upstream docker-compose.yml: same service names, container paths, hardening and healthcheck, with a fixed tag, a 127.0.0.1 binding and YAML anchors for the shared environment. Upstream also publishes 18790 and 3978 (bridge and Microsoft Teams); a Telegram-only agent needs neither.
x-openclaw-env: &openclaw-env
HOME: /home/node
OPENCLAW_HOME: /home/node
OPENCLAW_STATE_DIR: /home/node/.openclaw
OPENCLAW_CONFIG_PATH: /home/node/.openclaw/openclaw.json
OPENCLAW_CONFIG_DIR: /home/node/.openclaw
OPENCLAW_WORKSPACE_DIR: /home/node/.openclaw/workspace
OPENCLAW_GATEWAY_PORT: "18789"
OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN:-}
TZ: ${OPENCLAW_TZ:-UTC}
x-openclaw-volumes: &openclaw-volumes
- "${HOME}/.openclaw:/home/node/.openclaw"
- "${HOME}/.openclaw/workspace:/home/node/.openclaw/workspace"
- "${HOME}/.openclaw-auth-profile-secrets:/home/node/.config/openclaw"
services:
openclaw-gateway:
image: ghcr.io/openclaw/openclaw:2026.9.3
env_file:
- path: .env
required: false
environment: *openclaw-env
volumes: *openclaw-volumes
ports:
- "127.0.0.1:18789:18789"
cap_drop:
- NET_RAW
- NET_ADMIN
security_opt:
- no-new-privileges:true
extra_hosts:
- "host.docker.internal:host-gateway"
init: true
restart: unless-stopped
command: ["node", "dist/index.js", "gateway", "--bind", "lan", "--port", "18789"]
healthcheck:
test: ["CMD", "node", "dist/docker-healthcheck.js"]
interval: 30s
timeout: 5s
retries: 5
start_period: 20s
openclaw-cli:
image: ghcr.io/openclaw/openclaw:2026.9.3
network_mode: "service:openclaw-gateway"
env_file:
- path: .env
required: false
cap_drop:
- NET_RAW
- NET_ADMIN
security_opt:
- no-new-privileges:true
environment:
<<: *openclaw-env
BROWSER: echo
volumes: *openclaw-volumes
stdin_open: true
tty: true
init: true
entrypoint: ["node", "dist/index.js"]
depends_on:
- openclaw-gateway
--bind lan is not a mistake: the networking docs explain that inside a container lan lets the host reach the published port, while loopback is reachable only from inside the container's own namespace. Exposure is decided by the ports: line, and 127.0.0.1:18789:18789 keeps it on host loopback; the docs require gateway auth for any non-loopback bind, so the token is mandatory. The openclaw-cli service shares the gateway's network namespace, so docker compose run --rm openclaw-cli ... runs any openclaw command against the live gateway.
Step 3: secrets in .env
Compose reads .env next to the Compose file and, through env_file, injects every variable into both containers. Keep it at mode 600 and out of git.
OPENCLAW_GATEWAY_TOKEN=replace-with-a-long-random-string
OPENAI_API_KEY=sk-...
TELEGRAM_BOT_TOKEN=123456789:AA...
OPENCLAW_TZ=Europe/London
Generate the token with openssl rand -hex 32. OPENAI_API_KEY is the variable the Docker docs use for provider credentials; with --secret-input-mode ref onboarding stores a reference to that variable, not the key, and openclaw.json values also support ${VAR} substitution. For an OpenAI-compatible endpoint the onboard reference shows --auth-choice custom-api-key with --custom-base-url and --custom-model-id; for a provider without a documented non-interactive choice (Anthropic, as far as we could find) run the interactive wizard from the docs' manual flow: docker compose run --rm --no-deps --entrypoint node openclaw-gateway dist/index.js onboard --mode local --no-install-daemon. TELEGRAM_BOT_TOKEN is the documented fallback for the default Telegram account; config wins over it, and it must stay in .env after bootstrap because --use-env does not copy it into the config. The configuration docs also allow an optional ~/.openclaw/.env inside the state directory; we use it for MCP server headers.
Step 4: onboarding and Telegram
The Docker docs provide a non-interactive onboarding command for hosts without a TTY. It writes openclaw.json into the mounted directory, points gateway auth at the .env token and skips channels:
docker compose run -T --rm --no-deps --entrypoint node openclaw-gateway \
dist/index.js onboard --non-interactive --accept-risk --skip-health \
--mode local \
--auth-choice openai-api-key \
--secret-input-mode ref \
--gateway-auth token \
--gateway-token-ref-env OPENCLAW_GATEWAY_TOKEN \
--skip-channels \
--no-install-daemon
docker compose run -T --rm --no-deps --entrypoint node openclaw-gateway \
dist/index.js channels add --channel telegram --use-env
Both commands are copied from the Docker docs; --use-env validates that TELEGRAM_BOT_TOKEN is set before it writes config. Now lock the bot to yourself: Telegram identifies you by a numeric user id (DM the bot while it is still in pairing mode and read the id from its reply, or ask @userinfobot), and the access-control docs recommend dmPolicy: "allowlist" with that id in allowFrom, plus commands.ownerAllowFrom for owner-only commands; usernames, phone numbers and chat ids are not accepted. Edit ~/.openclaw/openclaw.json:
{
"channels": {
"telegram": {
"enabled": true,
"dmPolicy": "allowlist",
"allowFrom": ["123456789"]
}
},
"commands": {
"ownerAllowFrom": ["telegram:123456789"]
}
}
The default dmPolicy is pairing, which parks unknown senders until you approve them; allowlist is stricter and is what we ship. The file is JSON5, so comments and trailing commas are tolerated, but an unknown key or an invalid value makes the gateway refuse to start with exit code 78.
Step 5: start, check health, read logs
docker compose up -d openclaw-gateway
docker compose ps
docker compose logs -f openclaw-gateway
curl -fsS http://127.0.0.1:18789/healthz
curl -fsS http://127.0.0.1:18789/readyz
docker compose run --rm openclaw-cli gateway status
docker compose run --rm openclaw-cli doctor
The healthcheck runs node dist/docker-healthcheck.js every 30 seconds after a 20 second start period, so docker compose ps shows healthy once the gateway is up. The docs list three unauthenticated probes: /healthz (liveness), /startupz (startup) and /readyz (channel-aware readiness). A healthy gateway status prints Runtime: running and Connectivity probe: ok.
Locking the container down
OpenClaw is remote code execution by design: the agent runs shell commands for you, which is the point and also why the gateway must never be reachable from the internet. Earlier releases had CVE-2026-25253, a one-click remote code execution through gateway token theft, plus origin-validation flaws in the control UI. The defence is the same for every version: the port stays on loopback and you reach it through SSH or a private overlay network.
- Publish on
127.0.0.1only. The security docs point out that published container ports go through Docker's forwarding chains rather than only the hostINPUTrules, so a0.0.0.0publish can be open even when your firewall says otherwise; filter anything you do publish in theDOCKER-USERchain. - Reach the control UI through the SSH tunnel from the remote-access docs:
ssh -N -L 18789:127.0.0.1:18789 user@gateway-host, then openhttp://127.0.0.1:18789/. The docs prefer Tailscale Serve over LAN binds for the same reason: the gateway stays on loopback. - Restrict who may talk to the bot with
allowFrom, and what each sender may do withtools.toolsBySenderif more than one person is allowed. - Run
docker compose run --rm openclaw-cli security auditafter every change; the docs call it the one command that tells you whether you have drifted.
The sandbox caveat
OpenClaw can run tool execution inside a separate sandbox container. The setting is agents.defaults.sandbox.mode with three documented values: off (the default), non-main (every session except the agent's main session) and all (every session). The Docker backend creates those sandboxes through the docker CLI with hardened defaults: network none, read-only root, all capabilities dropped, image openclaw-sandbox:bookworm-slim, which you build yourself with scripts/sandbox-setup.sh (OpenClaw will not substitute a plain Debian image). The docs frame it as a wall around tool execution while the gateway itself stays on the host.
The catch: a containerised gateway has no Docker daemon of its own. The upstream Compose file spells it out in a comment: enabling the sandbox requires the Docker CLI in the image (build with --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1, or run setup.sh with OPENCLAW_SANDBOX=1), mounting /var/run/docker.sock into the gateway container and adding the host docker group id via group_add. The docs call the result sibling containers created through the host's Docker socket. Handing that socket to a container that runs an AI agent gives the agent root-equivalent control of the host, which undoes most of what the sandbox adds.
So most Docker deployments, ours included, run with the sandbox off and rely on two boundaries: the gateway container (unprivileged uid 1000, dropped capabilities, no-new-privileges) and the virtual machine around it, which holds nothing else. If you need the sandbox, install natively on a host where the gateway can reach Docker, as in our step-by-step VPS install guide, or use another documented backend: podman, ssh, openshell or crabbox.
The -browser image and memory
The standard image has no browser. For an agent that opens web pages, use the -browser variant; the docs' example is ghcr.io/openclaw/openclaw:latest-browser, and for a pinned deployment they say to choose a release's -browser tag instead of that moving one. The docs state that with a Docker gateway the browser binary must live inside the container; the browser image bakes in Playwright's Chromium plus Xvfb, which OpenClaw auto-detects on Linux; if startup reports a missing display, check browser.headless or OPENCLAW_BROWSER_HEADLESS. Do not mount over /home/node/.cache/ms-playwright, which would hide the bundled browser. The docs give no memory figure; in our deployments a headless Chromium session adds roughly 0.5 to 1 GB on top of the gateway, so we treat 4 GB as the minimum for the browser image.
Upgrading by retagging
Because the state is on the host, an upgrade is a tag change. Back up, edit the tag in both services (for example to 2026.9.4), then:
tar czf ~/openclaw-backup-$(date +%F).tgz -C ~ .openclaw
docker compose pull openclaw-gateway openclaw-cli
docker compose up -d openclaw-gateway
docker compose run --rm openclaw-cli doctor --json
The new gateway runs startup-safe migrations before it reports ready, and the docs say a routine image upgrade should not need a separate doctor --fix pass. If a repair cannot complete safely the gateway exits instead of reporting healthy and the container restarts in a loop; the documented fix is to run the same image once with doctor --fix as the command against the same state directory, then start the gateway and run doctor --json as the read-only preflight:
docker run --rm -v ~/.openclaw:/home/node/.openclaw ghcr.io/openclaw/openclaw:2026.9.4 openclaw doctor --fix
The tar archive is the plain-shell backup; the docs also offer openclaw backup create --verify. Rolling back means the previous tag plus that backup: the docs warn that a state database migrated to a newer version does not roll back on its own.
When to skip all of this
For an agent that answers you on Telegram, runs a few tools and stays private, the steps above are the whole job; a small Linux VM from our VPS range is enough to host it. If you would rather not maintain Docker at all, our ready OpenClaw server from 9.35 EUR per month ships with Ubuntu 24.04, the gateway preinstalled in Docker as described here, the port on loopback and the sandbox off; you add the LLM key, connect the Telegram bot and start talking. For which machine to pick, see choosing a server for AI workloads.
Questions
Do I need Node.js on the host to run OpenClaw in Docker?
No. The native install requires Node 24.16+ or 26.1+, but the image ships its own runtime; the host needs only Docker Engine and Compose v2.
Why does the Compose file bind to lan if the gateway should be loopback-only?
Inside a container, loopback means the container's own namespace, which the host cannot reach, so the networking docs default Docker deployments to lan and let the ports line decide exposure; publishing on 127.0.0.1:18789 keeps the gateway loopback-only as seen from the host.
Can I turn the sandbox on later?
Yes, but with the gateway in Docker it means giving the container the Docker CLI and the host's Docker socket to spawn sibling sandbox containers. For most single-owner agents the container plus VM boundary is the safer trade.
Where are the logs?
docker compose logs -f openclaw-gateway streams the gateway output; docker compose run --rm openclaw-cli logs --follow uses OpenClaw's own log command. Start with gateway status and doctor when something looks wrong.