Docker Setup for OpenClaw: Optional Containerized Gateway
Learn how to run OpenClaw in Docker for a self-contained, disposable environment. Covers prerequisites, building the image, and switching to Podman.
Read this when
- You want a containerized gateway instead of local installs
- You are validating the Docker flow
- You are migrating from ClawDock shell helpers
Docker is optional. Choose it when you want a self-contained, disposable gateway environment or a machine with nothing installed locally. If you typically develop on your own workstation, stick with the standard installation path.
The default Docker sandbox backend relies solely on the docker CLI. Switch the backend to "podman" to use native Podman directly. Sandboxing is disabled by default and does not require the gateway itself to be containerized. Additional sandbox backends for SSH and OpenShell exist; refer to Sandboxing.
Running multiple users? Check Multi-tenant hosting for the one-cell-per-tenant approach.
Prerequisites
- Docker Desktop (or Docker Engine) plus Docker Compose v2
- Minimum 2 GB RAM for building images (on 1 GB hosts,
pnpm installcan be OOM-killed with exit 137) - Sufficient storage for images and logs
- On a VPS or public-facing host, consult Security hardening for network exposure, paying attention to the Docker
DOCKER-USERfirewall chain
Containerized gateway
Build the image
From the repository root:
./scripts/docker/setup.sh
That command builds the gateway image locally under the name openclaw:local. To pull a pre-built image instead:
export OPENCLAW_IMAGE="ghcr.io/openclaw/openclaw:latest"
./scripts/docker/setup.sh
Pre-built images are first pushed to the GitHub Container Registry. GHCR serves as the primary registry for release automation, pinned deployments, and provenance checks. The same release also publishes a Docker Hub mirror at openclaw/openclaw:
export OPENCLAW_IMAGE="openclaw/openclaw:latest"
./scripts/docker/setup.sh
Stick with ghcr.io/openclaw/openclaw or openclaw/openclaw; unofficial mirrors don't follow OpenClaw's release cadence or retention policies. Version-specific tags cover releases like 2026.2.26 and prereleases like 2026.2.26-beta.1. Stable releases update latest and main; trailing-month Gateway releases only update extended-stable. Available variants include slim, main-slim, extended-stable-slim, latest-browser, main-browser, and extended-stable-browser. The standard images come with the codex and diagnostics-otel plugins bundled. A -browser variant additionally ships with Chromium baked in, handy for the sandboxed browser tool without needing a first-run Playwright install.
Airgapped rerun
On machines without network access, transfer and load the image beforehand:
docker load -i openclaw-image.tar
export OPENCLAW_IMAGE="ghcr.io/openclaw/openclaw:latest"
./scripts/docker/setup.sh --offline
--offline confirms OPENCLAW_IMAGE is already present locally, turns off implicit Compose pulls/builds, and then proceeds with the standard steps: .env sync, permission fixes, onboarding, gateway config sync, and Compose startup.
If OPENCLAW_SANDBOX=1 is set, offline setup also verifies the configured default and per-agent sandbox images on the daemon behind OPENCLAW_DOCKER_SOCKET, including the browser-contract label on Docker-backed browser images. When a required image is absent or outdated, setup exits without touching sandbox config instead of reporting a false success.
Complete onboarding
The setup script handles onboarding automatically:
- asks for provider API keys
- creates a gateway token and stores it in
.env - sets up the auth-profile secret key directory
- launches the gateway through Docker Compose
Pre-start onboarding and config writes go through openclaw-gateway directly (using --no-deps --entrypoint node), because openclaw-cli shares the gateway's network namespace and only functions once the gateway container is running.
Open the Control UI
Open http://127.0.0.1:18789/ and paste the token from .env into Settings. If you've switched the container to password auth, enter that password instead.
Need the URL again?
docker compose run --rm openclaw-cli dashboard --no-open
Configure channels (optional)
# WhatsApp (QR)
docker compose run --rm openclaw-cli channels login
# Telegram
docker compose run --rm openclaw-cli channels add --channel telegram --token "<token>"
# Discord
docker compose run --rm openclaw-cli channels add --channel discord --token "<token>"
Docs: WhatsApp, Telegram, Discord
Headless bootstrap
For a host that runs unattended, place provider, Gateway, and channel credentials in the Compose .env file so both the one-shot bootstrap container and the long-running Gateway get identical values:
OPENAI_API_KEY=<provider-key>
OPENCLAW_GATEWAY_TOKEN=<gateway-token>
TELEGRAM_BOT_TOKEN=<bot-token>
Run onboarding and channel provisioning without a pseudo-TTY, then start the Gateway:
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
docker compose up -d openclaw-gateway
The channel command aborts before it can alter any configuration when a plugin-declared environment variable is absent. After bootstrap, retain TELEGRAM_BOT_TOKEN inside .env: --use-env delegates credential resolution to the environment rather than embedding the token in openclaw.json, and the live Gateway must see the same variable. If channel config changes after startup, the Gateway's config watcher hot-reloads the affected channel on its own.
For credential-flag alternatives and other channel plugins, consult openclaw channels.
Manual flow
BUILD_GIT_COMMIT="$(git rev-parse HEAD)"
BUILD_TIMESTAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
docker build \
--build-arg "GIT_COMMIT=${BUILD_GIT_COMMIT}" \
--build-arg "OPENCLAW_BUILD_TIMESTAMP=${BUILD_TIMESTAMP}" \
-t openclaw:local -f Dockerfile .
docker compose run --rm --no-deps --entrypoint node openclaw-gateway \
dist/index.js onboard --mode local --no-install-daemon
docker compose run --rm --no-deps --entrypoint node openclaw-gateway \
dist/index.js config set --batch-json '[{"path":"gateway.mode","value":"local"},{"path":"gateway.bind","value":"lan"},{"path":"gateway.controlUi.allowedOrigins","value":["http://localhost:18789","http://127.0.0.1:18789"]}]'
docker compose up -d openclaw-gateway
The Docker context omits .git. Supply the source identity via build arguments, as demonstrated above, so the image's About screen shows the checked-out commit and a single build timestamp. scripts/docker/setup.sh resolves and passes both values automatically.
Note
Execute
docker composefrom the repository root. WhenOPENCLAW_EXTRA_MOUNTSorOPENCLAW_HOME_VOLUMEis enabled, the setup script generatesdocker-compose.extra.yml; place it after anydocker-compose.override.ymlyou keep yourself, for instance-f docker-compose.yml -f docker-compose.override.yml -f docker-compose.extra.yml.
Upgrading container images
Swapping the OpenClaw image while keeping the same mounted state and config makes the new gateway run startup-safe upgrade migrations and plugin convergence prior to readiness. Standard image upgrades should not demand a separate openclaw doctor --fix pass.
If startup cannot finish those repairs safely, the gateway exits rather than declaring itself healthy. Under a restart policy, Docker, Podman, or Kubernetes might show the gateway container restarting. Preserve the mounted state volume, then launch the same image once with openclaw doctor --fix as the container command, using the same state and config mounts the gateway relies on:
docker run --rm -v <openclaw-state>:/home/node/.openclaw <image> openclaw doctor --fix
podman run --rm -v <openclaw-state>:/home/node/.openclaw <image> openclaw doctor --fix
Once doctor completes, restart the gateway container with its default command. In Kubernetes, execute the same command in a one-off Job or debug pod attached to the same PVC, then restart the Deployment or StatefulSet.
After the container is back up, run the read-only deployment preflight against the same mounted state:
docker compose run --rm openclaw-cli doctor --json
Environment variables
Optional variables that scripts/docker/setup.sh accepts (and, for the gateway container, that docker-compose.yml takes directly):
| Variable | Purpose |
|---|---|
OPENCLAW_IMAGE | Pull a remote image instead of performing a local build |
OPENCLAW_IMAGE_APT_PACKAGES | Add extra apt packages during the build process (space-separated). Old name: OPENCLAW_DOCKER_APT_PACKAGES |
OPENCLAW_IMAGE_PIP_PACKAGES | Add extra Python packages during the build process (space-separated) |
OPENCLAW_EXTENSIONS | Build and package the chosen plugins that are supported, then install their runtime dependencies (ids separated by commas or spaces) |
OPENCLAW_DOCKER_BUILD_NODE_OPTIONS | Replace the default Node settings for local source builds (default --max-old-space-size=8192) |
OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB | Replace the default tsdown heap in MB for local source builds |
OPENCLAW_DOCKER_BUILD_SKIP_DTS | Omit declaration output when building runtime-only local images (default 1) |
OPENCLAW_INSTALL_BROWSER | Include Chromium and Xvfb in the image during the build stage |
OPENCLAW_EXTRA_MOUNTS | Additional host bind mounts (comma-separated source:target[:opts]) |
OPENCLAW_HOME_VOLUME | Keep /home/node in a named Docker volume |
OPENCLAW_TZ | Assign the gateway and CLI containers an IANA timezone name (default UTC) |
OPENCLAW_SANDBOX | Choose to enable sandbox bootstrap (1, true, yes, on) |
OPENCLAW_SKIP_ONBOARDING | Bypass the interactive onboarding flow (1, true, yes, on) |
OPENCLAW_DOCKER_SOCKET | Replace the default Docker socket path |
OPENCLAW_DISABLE_BONJOUR | Turn Bonjour/mDNS advertising on (0) or off (1); refer to Bonjour / mDNS |
OPENCLAW_DISABLE_BUNDLED_SOURCE_OVERLAYS | Turn off bind-mount overlays for bundled plugin source |
OTEL_EXPORTER_OTLP_ENDPOINT | One OTLP/HTTP collector endpoint shared by OpenTelemetry export |
OTEL_EXPORTER_OTLP_*_ENDPOINT | Separate OTLP endpoints for traces, metrics, or logs |
OTEL_EXPORTER_OTLP_PROTOCOL | Shared OTLP protocol fallback. Only http/protobuf is currently supported |
OTEL_EXPORTER_OTLP_*_PROTOCOL | Per-signal protocol fallback for traces, metrics, or logs; takes precedence over the shared fallback |
OTEL_SERVICE_NAME | Service name attached to OpenTelemetry resources |
OTEL_SEMCONV_STABILITY_OPT_IN | Opt in to the newest experimental GenAI semantic attributes |
OPENCLAW_OTEL_PRELOADED | Skip launching a second OpenTelemetry SDK when one is already loaded |
Homebrew is not included in the official image. During onboarding, OpenClaw hides installers for brew-only skill dependencies inside a Linux container lacking brew; supply those dependencies via a custom image or set them up by hand. For dependencies packaged by Debian, use OPENCLAW_IMAGE_APT_PACKAGES; for Python dependencies, use OPENCLAW_IMAGE_PIP_PACKAGES (which executes python3 -m pip install --break-system-packages during the build, so pin versions and only rely on indexes you trust).
When Docker reports ResourceExhausted, cannot allocate memory, or stops during tsdown, raise the Docker builder memory limit or retry using smaller explicit heaps:
OPENCLAW_DOCKER_BUILD_NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB=4096
Source-built images with selected plugins
OPENCLAW_EXTENSIONS picks plugin manifest ids from the source checkout;
existing source-directory names are also accepted when they differ. The Docker
build resolves the selection to source directories once, installs production
dependencies, and includes the selected plugin runtime in the image. Source
checkouts also compile first-party plugins published separately with
openclaw.build.bundledDist: false; that marker still preserves the plugin's
external npm or ClawHub ownership and does not change either artifact contract.
Unknown, invalid, or ambiguous ids fail the image build.
Known dependency/source-only ids keep their existing source and dependency
staging without gaining a compiled root dist entry. A selected plugin with
unified build entries must compile successfully; unselected external plugin
source and runtime output are pruned.
For example, these commands build separate, multi-architecture standalone
FakeCo gateway images for ClickClack, Slack, and Microsoft Teams. ClawRouter is
already part of the root OpenClaw runtime, so the ClickClack image selects only
clickclack. The explicit empty browser argument keeps the default image free
of Chromium:
SOURCE_SHA="$(git rev-parse HEAD)"
BUILD_TIMESTAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
REGISTRY="registry.example.com/fakeco"
build_gateway_image() {
gateway="$1"
selected_plugin="$2"
docker buildx build \
--platform linux/amd64,linux/arm64 \
--build-arg "GIT_COMMIT=${SOURCE_SHA}" \
--build-arg "OPENCLAW_BUILD_TIMESTAMP=${BUILD_TIMESTAMP}" \
--build-arg "OPENCLAW_EXTENSIONS=${selected_plugin}" \
--build-arg OPENCLAW_INSTALL_BROWSER= \
--provenance=mode=max \
--sbom=true \
--tag "${REGISTRY}/openclaw-${gateway}:${SOURCE_SHA}" \
--push \
.
}
build_gateway_image clickclack clickclack
build_gateway_image slack slack
build_gateway_image teams msteams
Use --platform linux/arm64 --load or --platform linux/amd64 --load for a
single native local build. Multi-platform output and attached SBOM/provenance
require a registry or another Buildx output that preserves attestations. After
pushing, inspect the manifest and deploy the immutable digest rather than the
mutable source-SHA tag:
docker buildx imagetools inspect \
"${REGISTRY}/openclaw-clickclack:${SOURCE_SHA}"
# Deploy: registry.example.com/fakeco/openclaw-clickclack@sha256:<manifest-digest>
These images are for standalone OCI-based gateways and generic Docker users. Crabhelm-managed gateways do not consume them: that delivery path builds a separate x86_64 appliance archive containing an OpenClaw npm tarball and pins the Node, archive, and manifest digests. Build that appliance independently from the same landed OpenClaw source.
To test bundled plugin source against a packaged image, mount one plugin source directory over its packaged source path, e.g. OPENCLAW_EXTRA_MOUNTS=/path/to/fork/extensions/synology-chat:/app/extensions/synology-chat:ro. That overrides the matching compiled /app/dist/extensions/synology-chat bundle for the same plugin id.
Observability
OpenTelemetry traffic flows outward from the Gateway container toward your OTLP collector, so no Docker port needs to be exposed for it. If you are building an image locally and want the bundled exporter included:
export OPENCLAW_EXTENSIONS="diagnostics-otel"
export OTEL_EXPORTER_OTLP_ENDPOINT="http://otel-collector:4318"
export OTEL_SERVICE_NAME="openclaw-gateway"
./scripts/docker/setup.sh
The official prebuilt images come with diagnostics-otel already bundled; only reinstall clawhub:@openclaw/diagnostics-otel if you stripped it out. To turn on export, permit and activate the diagnostics-otel plugin in your configuration, then assign diagnostics.otel.enabled=true (the complete example appears in OpenTelemetry export). Authentication headers for the collector are passed through diagnostics.otel.headers, never via Docker environment variables.
Prometheus metrics share the Gateway port that is already published. After installing clawhub:@openclaw/diagnostics-prometheus and switching on the diagnostics-prometheus plugin, scrape as follows:
http://<gateway-host>:18789/api/diagnostics/prometheus
Gateway authentication guards that route, so do not open a dedicated public /metrics port or an unauthenticated reverse-proxy path for it. More detail is in Prometheus metrics.
Health checks
Container probe endpoints (these require no authentication):
curl -fsS http://127.0.0.1:18789/healthz # liveness
curl -fsS http://127.0.0.1:18789/startupz # startup and traffic admission
curl -fsS http://127.0.0.1:18789/readyz # deep, channel-aware readiness
Inside the image, the built-in HEALTHCHECK checks /healthz; when failures keep occurring the container gets marked unhealthy, which lets orchestrators restart or swap it out.
Point an orchestrator startup or readiness probe at /startupz so a broken channel account never takes the healthy Gateway and Control UI out of rotation. Choose /readyz for monitoring that deliberately treats hard channel failures as not ready. Response specifics are covered in Health checks.
Authenticated deep health snapshot:
docker compose exec openclaw-gateway sh -lc 'node dist/index.js gateway health --token "$OPENCLAW_GATEWAY_TOKEN"'
LAN vs loopback
The scripts/docker/setup.sh default is OPENCLAW_GATEWAY_BIND=lan, so http://127.0.0.1:18789 on the host machine works alongside Docker port publishing.
lan(the default): the published gateway port is reachable from a host browser and host CLI.loopback: only processes within the container network namespace can reach the gateway directly.
Note
In
gateway.bind, stick to bind mode values (lan/loopback/custom/tailnet/auto); host aliases such as0.0.0.0or127.0.0.1will not work.
Host local providers
From inside the container, 127.0.0.1 points at the container itself, not the host machine. For providers running on the host, use host.docker.internal:
| Provider | Host default URL | Docker setup URL |
|---|---|---|
| LM Studio | http://127.0.0.1:1234 | http://host.docker.internal:1234 |
| Ollama | http://127.0.0.1:11434 | http://host.docker.internal:11434 |
Those URLs are what the bundled setup uses as the default onboarding values for LM Studio and Ollama, and docker-compose.yml translates host.docker.internal into the host gateway on Linux Docker Engine (Docker Desktop supplies the same alias on macOS and Windows). Any host service must be listening on an address Docker can actually reach:
lms server start --port 1234 --bind 0.0.0.0
OLLAMA_HOST=0.0.0.0:11434 ollama serve
Working from your own Compose file or docker run? Add the same mapping yourself, for instance --add-host=host.docker.internal:host-gateway.
Claude CLI backend in Docker
Claude Code is not installed in the official image. Install it and log in under the container's node user, then make that container home persistent so an image upgrade does not wipe the binary or authentication state.
For a fresh setup, attach a persistent /home/node volume before running the setup routine:
export OPENCLAW_IMAGE="ghcr.io/openclaw/openclaw:latest"
export OPENCLAW_HOME_VOLUME="openclaw_home"
./scripts/docker/setup.sh
For an existing setup, stop the stack and reload the current .env values first, since the setup script always regenerates .env from the shell and defaults at that moment, never from reading the file:
set -a
. ./.env
set +a
export OPENCLAW_HOME_VOLUME="${OPENCLAW_HOME_VOLUME:-openclaw_home}"
./scripts/docker/setup.sh
If .env holds values your shell cannot source, manually re-export what you depend on first (OPENCLAW_IMAGE, ports, bind mode, custom paths, OPENCLAW_EXTRA_MOUNTS, sandbox, skip-onboarding). The generated overlay mounts the home volume for both openclaw-gateway and openclaw-cli; run the remaining commands with that overlay in place (and start with docker-compose.override.yml if you use one).
docker compose -f docker-compose.yml -f docker-compose.extra.yml run --rm \
--entrypoint sh openclaw-cli -lc \
'curl -fsSL https://claude.ai/install.sh | bash'
The native installer places its files in claude, which maps to /home/node/.local/bin/claude. Because the OpenClaw image ships /home/node/.local/bin on PATH, the included Anthropic plugin finds it without needing an adapter config override.
After logging in, verify from the same persisted home directory:
docker compose -f docker-compose.yml -f docker-compose.extra.yml run --rm \
--entrypoint /home/node/.local/bin/claude openclaw-cli auth login
docker compose -f docker-compose.yml -f docker-compose.extra.yml run --rm \
--entrypoint /home/node/.local/bin/claude openclaw-cli auth status --text
docker compose -f docker-compose.yml -f docker-compose.extra.yml run --rm \
openclaw-cli models auth login \
--provider anthropic --method cli --set-default
docker compose -f docker-compose.yml -f docker-compose.extra.yml run --rm \
openclaw-cli models list --provider anthropic
Next, switch to the bundled claude-cli backend:
docker compose -f docker-compose.yml -f docker-compose.extra.yml run --rm \
openclaw-cli agent \
--agent main \
--model claude-cli/claude-sonnet-4-6 \
--message "Say hello from Docker Claude CLI"
OPENCLAW_HOME_VOLUME keeps the native install under /home/node/.local/bin and /home/node/.local/share/claude, while Claude Code settings and auth live under /home/node/.claude and /home/node/.claude.json. Persisting just /home/node/.openclaw won't cover everything; if you go with OPENCLAW_EXTRA_MOUNTS instead of a home volume, make sure both services mount every one of those Claude paths.
Note
For shared production automation or predictable Anthropic billing, go with the Anthropic API-key route. Reusing the Claude CLI ties you to Claude Code's installed version, account login, billing, and update behavior.
Bonjour / mDNS
Docker bridge networking typically fails to forward Bonjour/mDNS multicast (224.0.0.251:5353) in a reliable way. With OPENCLAW_DISABLE_BONJOUR left unset, the bundled Bonjour plugin turns off LAN advertising on its own once it detects a container environment, so it avoids crash-looping on multicast that the bridge drops. Set OPENCLAW_DISABLE_BONJOUR=1 to disable it regardless of detection, or 0 to enable it explicitly (only on host networking, macvlan, or another network where mDNS multicast is confirmed to work).
For Docker hosts, use the published Gateway URL, Tailscale, or wide-area DNS-SD instead. See Bonjour discovery for gotchas and troubleshooting.
Storage and persistence
Docker Compose bind-mounts OPENCLAW_CONFIG_DIR to /home/node/.openclaw, OPENCLAW_WORKSPACE_DIR to /home/node/.openclaw/workspace, and OPENCLAW_AUTH_PROFILE_SECRET_DIR to /home/node/.config/openclaw, so those paths persist across container replacement. When a variable isn't set, docker-compose.yml defaults to ${HOME}, or /tmp if HOME itself is absent, so docker compose up never produces an empty-source volume spec on bare environments.
That mounted config directory contains:
openclaw.jsonfor behavior configagents/<agentId>/agent/auth-profiles.jsonfor stored provider OAuth/API-key auth.envfor env-backed runtime secrets such asOPENCLAW_GATEWAY_TOKEN
The auth-profile secret directory holds the local encryption key for OAuth-backed auth profile token material. Keep it with your Docker host state, but separate from OPENCLAW_CONFIG_DIR.
Installed downloadable plugins keep package state under the mounted OpenClaw home, so install records and package roots survive container replacement; gateway startup does not regenerate bundled-plugin dependency trees.
For full VM persistence details, see Docker VM Runtime - What persists where.
Disk growth hotspots: media/, per-agent SQLite databases, legacy session JSONL transcripts, the shared SQLite state database, installed plugin package roots, and rolling file logs under /tmp/openclaw/.
Shell helpers (optional)
For shorter day-to-day commands, install ClawDock:
mkdir -p ~/.clawdock && curl -sL https://raw.githubusercontent.com/openclaw/openclaw/main/scripts/clawdock/clawdock-helpers.sh -o ~/.clawdock/clawdock-helpers.sh
echo 'source ~/.clawdock/clawdock-helpers.sh' >> ~/.zshrc && source ~/.zshrc
If you came from the older scripts/shell-helpers/clawdock-helpers.sh path, rerun the command above so your local helper tracks the current location. Then use clawdock-start, clawdock-stop, clawdock-dashboard, etc. (run clawdock-help for the full list).
Enable agent sandbox for Docker gateway
export OPENCLAW_SANDBOX=1
./scripts/docker/setup.sh
Custom socket path (e.g. rootless Docker):
export OPENCLAW_SANDBOX=1
export OPENCLAW_DOCKER_SOCKET=/run/user/1000/docker.sock
./scripts/docker/setup.sh
The script mounts docker.sock only after sandbox prerequisites pass. If sandbox setup can't complete, it resets agents.defaults.sandbox.mode to off. Codex code mode is disabled for turns where the OpenClaw sandbox is active (see Sandboxing § Docker backend); never mount the host Docker socket into agent sandbox containers.
Automation / CI (non-interactive)
Disable Compose pseudo-TTY allocation with -T:
docker compose run -T --rm openclaw-cli gateway probe
docker compose run -T --rm openclaw-cli devices list --json
Shared-network security note
openclaw-cli relies on network_mode: "service:openclaw-gateway" so that CLI commands can talk to the gateway over 127.0.0.1. Consider this a shared trust boundary. The compose configuration removes NET_RAW/NET_ADMIN and turns on no-new-privileges for both openclaw-gateway and openclaw-cli.
Docker Desktop DNS failures in openclaw-cli
Certain Docker Desktop configurations encounter DNS lookup failures from the shared-network openclaw-cli sidecar after NET_RAW is removed, surfacing as EAI_AGAIN during npm-backed operations such as openclaw plugins install. Stick with the default hardened compose file for regular use. The override that follows restores standard capabilities for the openclaw-cli container only, apply it to the one-off command that needs registry access rather than as your standard invocation:
printf '%s\n' \
'services:' \
' openclaw-cli:' \
' cap_drop: !reset []' \
> docker-compose.cli-no-dropped-caps.local.yml
docker compose -f docker-compose.yml -f docker-compose.cli-no-dropped-caps.local.yml run --rm openclaw-cli plugins install <package>
If a long-running openclaw-cli container already exists, recreate it using the same override, since docker compose exec/docker exec cannot alter Linux capabilities on a container that has already been created.
Permissions and EACCES
The image executes as node (uid 1000). When permission errors appear on /home/node/.openclaw, verify that your host bind mounts are owned by uid 1000:
sudo chown -R 1000:1000 /path/to/openclaw-config /path/to/openclaw-workspace
That same mismatch can also present as blocked plugin candidate: suspicious ownership (... uid=1000, expected uid=0 or root) followed by plugin present but blocked, where the process uid and the mounted plugin directory owner do not align. Stay with the default uid 1000 and correct the bind mount ownership. Only chown /path/to/openclaw-config/npm to root:root if you plan to run OpenClaw as root for an extended period.
Faster rebuilds
Go with the repo-root Dockerfile rather than swapping it out for a shortened single-stage example. Its workspace-deps stage pulls out the package manifests that pnpm-workspace.yaml needs, and then the build stage copies those manifests ahead of pnpm install --frozen-lockfile. This keeps the dependency layer cacheable without leaving out packages/*, selected extensions/*, or other required workspace metadata.
That same Dockerfile upholds the production runtime contract: digest-pinned Node and Bun bases, non-root uid 1000, tini, the built-in health check, and the /usr/local/bin/openclaw symlink. Dependabot keeps the reviewed base digests current; avoid swapping them for floating FROM node:24-bookworm tags.
Power-user container options
The default image prioritizes security and operates as non-root node. For a container with more features:
- Keep
/home/node:export OPENCLAW_HOME_VOLUME="openclaw_home" - Add system deps at build time:
export OPENCLAW_IMAGE_APT_PACKAGES="git curl jq" - Add Python deps at build time:
export OPENCLAW_IMAGE_PIP_PACKAGES="requests==2.32.5 humanize==4.14.0" - Add Playwright Chromium at build time:
export OPENCLAW_INSTALL_BROWSER=1, or go with the official-browserimage tag - Keep browser downloads and caches: use
OPENCLAW_HOME_VOLUMEorOPENCLAW_EXTRA_MOUNTS. On Linux, OpenClaw automatically detects the image's Playwright-managed Chromium.
OpenAI Codex OAuth (headless Docker)
Choosing OpenAI Codex OAuth in the wizard triggers a browser URL. In Docker or headless environments, copy the full redirect URL you end up on and paste it back into the wizard to complete authentication.
Base image metadata
The runtime image uses node:24-bookworm-slim and starts tini as PID 1 so zombie processes get reaped and signals are handled properly in long-running containers. It publishes OCI base-image annotations, including org.opencontainers.image.base.name and org.opencontainers.image.source. Dependabot refreshes the pinned Node base digest, and every build applies the latest Debian point-release updates. See OCI image annotations.
Image contents and security scanning
Runtime images carry only production Node.js dependencies. Release builds fix the base image by digest and apply current Debian security updates with apt-get dist-upgrade; the -browser variant installs the Chromium version tied to its Playwright release.
Scanner totals may include Debian findings that the distribution labels wont-fix. To rebuild locally against current base and package metadata, execute docker build --pull -t openclaw:local ..
Weekly image refreshes
The latest*, main*, and extended-stable* moving tags get rebuilt every week from the same tagged release source, which means they incorporate current OS security patches between OpenClaw releases. Stable and extended-stable refreshes stay separate, and beta images do not follow this rebuild schedule.
A dated tag, such as 2026.8.1-r20260820 (along with the -slim and -browser variants), is also published with each refresh. Plain version tags and dated -rYYYYMMDD tags cannot be changed; use either form when you want a deployment to stay fixed rather than track a moving tag.
Running on a VPS?
For shared VM deployment steps that cover binary baking, persistence, and updates, check Hetzner (Docker VPS) and Docker VM Runtime.
Agent sandbox
When agents.defaults.sandbox is turned on with the Docker backend, the gateway keeps itself on the host while agent tool execution (shell, file read/write, and similar operations) runs inside isolated Docker containers. This creates a hard boundary around untrusted or multi-tenant agent sessions without requiring the entire gateway to be containerized.
Sandbox scope defaults to per-agent, but per-session and shared options are available; each scope has its own workspace mounted at /workspace. You can also set up allow/deny tool policies, network isolation, resource limits, and browser containers.
For complete configuration, images, security notes, and multi-agent profiles:
- Sandboxing, full sandbox reference
- OpenShell, interactive shell access to sandbox containers
- Multi-Agent Sandbox and Tools, per-agent overrides
Quick enable
{
agents: {
defaults: {
sandbox: {
mode: "non-main", // off | non-main | all
scope: "agent", // session | agent | shared
},
},
},
}
To build the default sandbox image from a source checkout:
scripts/sandbox-setup.sh
If you are doing npm installs without a source checkout, the inline docker build commands in Sandboxing § Images and setup cover that path.
Troubleshooting
Image missing or sandbox container not starting
You can build the sandbox image using scripts/sandbox-setup.sh with a source checkout, the inline docker build command from Sandboxing § Images and setup with npm install, or by setting agents.defaults.sandbox.docker.image to your own custom image. Containers get created automatically per session when needed.
Permission errors in sandbox
Set docker.user to a UID:GID that matches the ownership of your mounted workspace, or run chown on the workspace folder.
Custom tools not found in sandbox
OpenClaw executes commands with sh -lc (a login shell), which pulls in /etc/profile and can reset PATH. To prepend your custom tool paths, set docker.env.PATH, or place a script under /etc/profile.d/ in your Dockerfile.
OOM-killed during image build (exit 137)
The VM requires at least 2 GB of RAM. Pick a larger machine class and try again.
Unauthorized or pairing required in Control UI
Get a fresh dashboard link and approve the browser device:
docker compose run --rm openclaw-cli dashboard --no-open
docker compose run --rm openclaw-cli devices list
docker compose run --rm openclaw-cli devices approve <requestId>
For more details: Dashboard, Devices.
Gateway target shows ws://172.x.x.x or pairing errors from Docker CLI
Reset gateway mode and bind:
docker compose run --rm openclaw-cli config set --batch-json '[{"path":"gateway.mode","value":"local"},{"path":"gateway.bind","value":"lan"}]'
docker compose run --rm openclaw-cli devices list --url ws://127.0.0.1:18789
Related
- Install Overview, every installation approach
- Podman, Podman as a Docker alternative
- ClawDock, Docker Compose community setup
- Updating, keeping OpenClaw current
- Configuration, gateway configuration after install