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 install can 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-USER firewall 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 compose from the repository root. When OPENCLAW_EXTRA_MOUNTS or OPENCLAW_HOME_VOLUME is enabled, the setup script generates docker-compose.extra.yml; place it after any docker-compose.override.yml you 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):

VariablePurpose
OPENCLAW_IMAGEPull a remote image instead of performing a local build
OPENCLAW_IMAGE_APT_PACKAGESAdd extra apt packages during the build process (space-separated). Old name: OPENCLAW_DOCKER_APT_PACKAGES
OPENCLAW_IMAGE_PIP_PACKAGESAdd extra Python packages during the build process (space-separated)
OPENCLAW_EXTENSIONSBuild and package the chosen plugins that are supported, then install their runtime dependencies (ids separated by commas or spaces)
OPENCLAW_DOCKER_BUILD_NODE_OPTIONSReplace the default Node settings for local source builds (default --max-old-space-size=8192)
OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MBReplace the default tsdown heap in MB for local source builds
OPENCLAW_DOCKER_BUILD_SKIP_DTSOmit declaration output when building runtime-only local images (default 1)
OPENCLAW_INSTALL_BROWSERInclude Chromium and Xvfb in the image during the build stage
OPENCLAW_EXTRA_MOUNTSAdditional host bind mounts (comma-separated source:target[:opts])
OPENCLAW_HOME_VOLUMEKeep /home/node in a named Docker volume
OPENCLAW_TZAssign the gateway and CLI containers an IANA timezone name (default UTC)
OPENCLAW_SANDBOXChoose to enable sandbox bootstrap (1, true, yes, on)
OPENCLAW_SKIP_ONBOARDINGBypass the interactive onboarding flow (1, true, yes, on)
OPENCLAW_DOCKER_SOCKETReplace the default Docker socket path
OPENCLAW_DISABLE_BONJOURTurn Bonjour/mDNS advertising on (0) or off (1); refer to Bonjour / mDNS
OPENCLAW_DISABLE_BUNDLED_SOURCE_OVERLAYSTurn off bind-mount overlays for bundled plugin source
OTEL_EXPORTER_OTLP_ENDPOINTOne OTLP/HTTP collector endpoint shared by OpenTelemetry export
OTEL_EXPORTER_OTLP_*_ENDPOINTSeparate OTLP endpoints for traces, metrics, or logs
OTEL_EXPORTER_OTLP_PROTOCOLShared OTLP protocol fallback. Only http/protobuf is currently supported
OTEL_EXPORTER_OTLP_*_PROTOCOLPer-signal protocol fallback for traces, metrics, or logs; takes precedence over the shared fallback
OTEL_SERVICE_NAMEService name attached to OpenTelemetry resources
OTEL_SEMCONV_STABILITY_OPT_INOpt in to the newest experimental GenAI semantic attributes
OPENCLAW_OTEL_PRELOADEDSkip 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 as 0.0.0.0 or 127.0.0.1 will 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:

ProviderHost default URLDocker setup URL
LM Studiohttp://127.0.0.1:1234http://host.docker.internal:1234
Ollamahttp://127.0.0.1:11434http://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.json for behavior config
  • agents/<agentId>/agent/auth-profiles.json for stored provider OAuth/API-key auth
  • .env for env-backed runtime secrets such as OPENCLAW_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:

  1. Keep /home/node: export OPENCLAW_HOME_VOLUME="openclaw_home"
  2. Add system deps at build time: export OPENCLAW_IMAGE_APT_PACKAGES="git curl jq"
  3. Add Python deps at build time: export OPENCLAW_IMAGE_PIP_PACKAGES="requests==2.32.5 humanize==4.14.0"
  4. Add Playwright Chromium at build time: export OPENCLAW_INSTALL_BROWSER=1, or go with the official -browser image tag
  5. Keep browser downloads and caches: use OPENCLAW_HOME_VOLUME or OPENCLAW_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:

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
4,371 words · updated Aug 17, 2026