How to Set Up Headless OpenClaw with Docker Compose

Run the prebuilt OpenClaw image headlessly using only Docker Compose, managed environment variables, named volumes, and a container console.

Article guideContents, topics, tags, and RSS
Get new notes via RSS

You can run OpenClaw on a managed Docker server even when you cannot access the host filesystem or connect over SSH. All you need is permission to deploy a Compose file, define environment variables, inspect logs, restart the service, and open a console inside the container.

This guide pulls the official prebuilt OpenClaw image from GitHub Container Registry. It does not clone the OpenClaw repository, build an image, create host directories, or run docker compose commands on the server.

Reviewed: 16 September 2026 against OpenClaw 2026.9.4. OpenClaw changes quickly, so compare this standalone file with the current OpenClaw Docker guide before a production deployment or upgrade. The configuration and commands were checked against the versioned source and documentation; this is not an end-to-end deployment test on every control panel.

The deployment model

The Compose service has two jobs:

  1. On its first start, it runs OpenClaw’s non-interactive onboarding command and saves the configuration in a named volume.
  2. After onboarding succeeds, it writes a completion marker and starts the long-running Gateway.

On later restarts or image upgrades, the marker remains in the volume, so onboarding is skipped and the Gateway starts normally.

This approach is intended for control panels such as Portainer, Dockge, Coolify, or another platform that can:

  • deploy or update a Compose stack;
  • inject protected environment variables;
  • preserve Docker named volumes;
  • show container logs and health;
  • restart or redeploy a container;
  • open a shell inside the running container; and
  • route an HTTPS hostname or private network connection to container port 18789.

If the platform cannot persist and back up volumes, or cannot restrict Gateway access to trusted clients, resolve that before treating the deployment as production-ready. Prefer private HTTPS ingress over a VPN or an access-controlled proxy. HTTPS encrypts traffic; it does not by itself make an internet-facing operator console private.

Choose the model provider

Configure the following variables in the platform’s stack environment or secret manager. Do not paste real credentials directly into a Compose file that will be stored in source control.

Native OpenAI

Native OpenAI
VariableValue
OPENCLAW_GATEWAY_TOKENA randomly generated secret of at least 32 bytes
OPENCLAW_SETUP_PROVIDERopenai
OPENAI_API_KEYYour OpenAI API key
OPENCLAW_PUBLIC_ORIGINThe exact UI origin, such as https://openclaw.example.com
OPENCLAW_TZAn IANA timezone, such as Europe/Stockholm

Generate the Gateway token with the platform’s secret generator or a password manager. It authenticates clients to OpenClaw and is separate from the model-provider key.

OPENCLAW_PUBLIC_ORIGIN must contain only the scheme, hostname, and optional port—no path or trailing slash. Leave it empty only for a channel-only deployment: the wrapper then explicitly disables the Control UI. An enabled UI on a non-loopback bind requires an allowed origin, otherwise the Gateway refuses to start.

Custom OpenAI-compatible API

For a self-hosted model server, internal proxy, or another API that implements an OpenAI protocol, keep the common Gateway, origin, and timezone variables above, then replace the native provider settings with:

Custom OpenAI-compatible API
VariableValue
OPENCLAW_SETUP_PROVIDERcustom
OPENCLAW_CUSTOM_BASE_URLAPI root, such as https://llm.example.com/v1
OPENCLAW_CUSTOM_MODEL_IDThe exact model ID accepted by the API
OPENCLAW_CUSTOM_PROVIDER_IDA stable short name, such as private-openai
OPENCLAW_CUSTOM_COMPATIBILITYopenai or openai-responses
CUSTOM_API_KEYThe API key, when the endpoint requires one

Use openai for an API that implements /v1/chat/completions. Use openai-responses only when it implements /v1/responses. Pass the API root as the base URL, not the complete request path.

If an intentionally private endpoint requires no authentication, leave CUSTOM_API_KEY empty. Never expose a keyless model endpoint to an untrusted network.

The base URL must be reachable from inside the OpenClaw container. Use a public or private HTTPS URL, or a Compose service name such as http://vllm:8000/v1 when the model server is part of the same stack. Do not use 127.0.0.1 for a different container or server; inside OpenClaw, it refers to the OpenClaw container itself.

Paste the Compose file

Create a new stack in the Docker control panel and use this compose.yml:

services:
  openclaw-gateway:
    image: ${OPENCLAW_IMAGE:-ghcr.io/openclaw/openclaw:2026.9.4}
    environment:
      HOME: /home/node
      OPENCLAW_HOME: /home/node
      TERM: xterm-256color
      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_BIND: lan
      OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN:?Set OPENCLAW_GATEWAY_TOKEN}
      OPENCLAW_SETUP_PROVIDER: ${OPENCLAW_SETUP_PROVIDER:-openai}
      OPENAI_API_KEY: ${OPENAI_API_KEY:-}
      CUSTOM_API_KEY: ${CUSTOM_API_KEY:-}
      OPENCLAW_CUSTOM_BASE_URL: ${OPENCLAW_CUSTOM_BASE_URL:-}
      OPENCLAW_CUSTOM_MODEL_ID: ${OPENCLAW_CUSTOM_MODEL_ID:-}
      OPENCLAW_CUSTOM_PROVIDER_ID: ${OPENCLAW_CUSTOM_PROVIDER_ID:-}
      OPENCLAW_CUSTOM_COMPATIBILITY: ${OPENCLAW_CUSTOM_COMPATIBILITY:-openai}
      OPENCLAW_PUBLIC_ORIGIN: ${OPENCLAW_PUBLIC_ORIGIN:-}
      TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
      TZ: ${OPENCLAW_TZ:-UTC}
    volumes:
      - openclaw-state:/home/node/.openclaw
      - openclaw-workspace:/home/node/.openclaw/workspace
      - openclaw-auth-profiles:/home/node/.config/openclaw
    cap_drop:
      - NET_RAW
      - NET_ADMIN
    security_opt:
      - no-new-privileges:true
    extra_hosts:
      - "host.docker.internal:host-gateway"
    expose:
      - "18789"
    init: true
    restart: unless-stopped
    command:
      - /bin/sh
      - -eu
      - -c
      - |
          marker=/home/node/.openclaw/.compose-onboarding-complete

          if [ ! -f "$$marker" ]; then
            case "$${OPENCLAW_SETUP_PROVIDER}" in
              openai)
                node 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
                ;;
              custom)
                : "$${OPENCLAW_CUSTOM_BASE_URL:?Set OPENCLAW_CUSTOM_BASE_URL}"
                : "$${OPENCLAW_CUSTOM_MODEL_ID:?Set OPENCLAW_CUSTOM_MODEL_ID}"
                : "$${OPENCLAW_CUSTOM_PROVIDER_ID:?Set OPENCLAW_CUSTOM_PROVIDER_ID}"

                node dist/index.js onboard \
                  --non-interactive \
                  --accept-risk \
                  --skip-health \
                  --mode local \
                  --auth-choice custom-api-key \
                  --custom-base-url "$${OPENCLAW_CUSTOM_BASE_URL}" \
                  --custom-model-id "$${OPENCLAW_CUSTOM_MODEL_ID}" \
                  --custom-provider-id "$${OPENCLAW_CUSTOM_PROVIDER_ID}" \
                  --custom-compatibility "$${OPENCLAW_CUSTOM_COMPATIBILITY}" \
                  --secret-input-mode ref \
                  --gateway-auth token \
                  --gateway-token-ref-env OPENCLAW_GATEWAY_TOKEN \
                  --skip-channels \
                  --no-install-daemon
                ;;
              *)
                echo "OPENCLAW_SETUP_PROVIDER must be openai or custom" >&2
                exit 1
                ;;
            esac

            if [ -n "$${OPENCLAW_PUBLIC_ORIGIN:-}" ]; then
              origins_json="$$(node -e 'process.stdout.write(JSON.stringify([process.env.OPENCLAW_PUBLIC_ORIGIN]))')"
              node dist/index.js config set \
                gateway.controlUi.allowedOrigins \
                "$$origins_json" \
                --strict-json
              node dist/index.js config set gateway.controlUi.enabled true --strict-json
            else
              node dist/index.js config set gateway.controlUi.enabled false --strict-json
            fi

            if [ -n "$${TELEGRAM_BOT_TOKEN:-}" ]; then
              node dist/index.js channels add \
                --channel telegram \
                --use-env
            fi

            touch "$$marker"
          fi

          exec node dist/index.js gateway \
            --bind "$${OPENCLAW_GATEWAY_BIND}" \
            --port 18789
    healthcheck:
      test: ["CMD", "node", "dist/docker-healthcheck.js"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 2m

volumes:
  openclaw-state:
  openclaw-workspace:
  openclaw-auth-profiles:

This is a standalone adaptation of OpenClaw’s versioned official Compose definition. The image property points directly to the prebuilt release and there is no build property or source checkout.

The standard image runs as the non-root node user with UID 1000. The image pre-creates the three named-volume mount points with the required ownership, so fresh, empty Docker-managed volumes do not need host-side mkdir, chown, or chmod commands. Existing or restored volumes retain their previous ownership; if startup reports EACCES, have the platform administrator repair those volumes rather than running the Gateway as root.

OPENCLAW_SETUP_PROVIDER, OPENCLAW_CUSTOM_*, and OPENCLAW_PUBLIC_ORIGIN are inputs to the first-start wrapper in this Compose file. OPENCLAW_IMAGE is a Compose image selector. The provider keys and OPENCLAW_GATEWAY_TOKEN remain runtime environment variables because the saved OpenClaw configuration refers back to them.

The $$ sequences in the command are intentional. Compose converts them to a single $ for the shell running inside the container; without that escaping, the control panel may try to expand the shell variables while parsing the stack.

Deploy and follow the logs

Enter all required variables before deploying the stack. The first startup should show these phases in the container log:

  1. non-interactive onboarding;
  2. Control UI origin configuration, or disabling the UI for a channel-only deployment;
  3. optional Telegram channel configuration; and
  4. Gateway startup on port 18789.

The container writes .compose-onboarding-complete only after every setup step succeeds. If a required variable is absent or onboarding fails, the container exits and the restart policy retries it. Correct the stack variables and redeploy; do not create the marker manually.

Once the Gateway starts, the health check should change to healthy. OpenClaw exposes /healthz for liveness, /startupz for startup and traffic admission, and /readyz for deeper channel-aware readiness. The included probe checks liveness, not whether a model request succeeds. Docker Compose does not restart a still-running container just because it becomes unhealthy; configure monitoring or the platform’s own recovery policy separately.

Verify OpenClaw from the container console

Open the platform’s console for openclaw-gateway. The image runs as the unprivileged node user and includes the openclaw command.

Run:

openclaw doctor --json
openclaw models status
openclaw security audit
openclaw agent \
  --agent main \
  --message "Reply with only: OpenClaw is running"

doctor --json is read-only in this release. Inspect its ok and findings fields: generating an advisory report can exit successfully even when it reports problems. For an automated gate with failure exit codes, use openclaw doctor --lint --json. Review the security audit before admitting users.

The last command selects the initial main agent created by this guide and makes a real provider request, which may incur usage charges. If the health check succeeds but this request fails, inspect the model ID, API base URL, protocol choice, and provider credential.

Non-interactive custom-provider onboarding validates and writes the configuration but does not guarantee a live model call. Always run this small request before connecting real users.

Access the Control UI without SSH

The Compose file does not publish port 18789 on the host. expose documents the container port; it is not a firewall or an instruction that automatically connects an external proxy. The ingress proxy must share a Docker network with the service, and other containers on that network can reach the listener. Use the platform’s private or access-controlled HTTPS ingress:

  1. Route an HTTPS hostname to service openclaw-gateway on container port 18789.
  2. Enable WebSocket forwarding.
  3. Set OPENCLAW_PUBLIC_ORIGIN to that exact HTTPS origin before the first deployment.
  4. Open the URL and paste OPENCLAW_GATEWAY_TOKEN into the Control UI settings.

If the external origin changes after the first successful startup, update it from the container console and restart through the platform. To enable the UI after a channel-only deployment, set the origin first and then enable it:

openclaw config set \
  gateway.controlUi.allowedOrigins \
  '["https://openclaw.example.com"]' \
  --strict-json
openclaw config set gateway.controlUi.enabled true --strict-json

If the platform supplies forwarded client headers, configure gateway.trustedProxies for only the ingress proxy addresses documented by that platform. Ensure the proxy overwrites forwarded client headers rather than trusting client-supplied values. Keep token authentication enabled; setting trusted proxy addresses does not require switching to trusted-proxy authentication. Do not trust every private subnet merely to make warnings disappear.

If there is no managed ingress, a platform administrator can bind a host port to a specific private interface. Set OPENCLAW_PRIVATE_BIND_IP to an address actually assigned to the Docker host (not 0.0.0.0), then replace the expose block with:

ports:
  - "${OPENCLAW_PRIVATE_BIND_IP:?Set the Docker host private IP}:18789:18789"

The shorter 18789:18789 mapping binds to all host interfaces, not just the private network. A private-IP binding still needs firewall and routing controls; Docker-published ports can bypass ordinary UFW rules. See Docker port publishing and Docker packet filtering.

Set OPENCLAW_PUBLIC_ORIGIN to the exact URL used by the client. Prefer HTTPS even on a private network: plain HTTP exposes the Gateway token and conversation traffic to on-path observers. In the reviewed version, browser device pairing can work over HTTP, but secure-context features such as passkeys still need HTTPS. Never expose this plaintext port directly to the internet.

When the Control UI asks for device approval, use the container console:

openclaw devices list
openclaw devices approve <requestId>

Add Telegram

If TELEGRAM_BOT_TOKEN exists during the first deployment, the bootstrap command adds the default Telegram account automatically. --use-env supports that default account; use the Telegram account configuration for additional accounts. Direct messages use pairing by default. Send the bot a message, then approve its code from the container console:

openclaw pairing list telegram
openclaw pairing approve telegram <code>

To add Telegram later:

  1. Add TELEGRAM_BOT_TOKEN to the platform’s protected environment.
  2. Redeploy so the running container receives the new variable.
  3. Open its console and run:
openclaw channels add --channel telegram --use-env

The official Telegram guide covers group policies, allowlists, and additional channel settings.

Optionally expose an OpenAI-compatible API

The custom-provider settings above tell OpenClaw which upstream model API to call. The reverse integration is different: another application can call an OpenClaw agent through POST /v1/chat/completions.

The endpoint is disabled by default. Keep it restricted to a VPN, tailnet, or private ingress; do not expose it to the public internet, even behind HTTPS and a bearer token. Enable it from the container console:

openclaw config set \
  gateway.http.endpoints.chatCompletions.enabled \
  true \
  --strict-json

Restart the container from the platform, then call the endpoint from your workstation or API client through your private HTTPS origin:

curl -sS https://openclaw.example.com/v1/chat/completions \
  -H "Authorization: Bearer <gateway-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openclaw/default",
    "messages": [
      {"role": "user", "content": "Reply with only: API is working"}
    ]
  }'

Here, openclaw/default follows the configured default agent rather than naming an agent literally called default. The model field selects an OpenClaw agent rather than the raw upstream model. The agent then uses the native OpenAI or custom provider configured during onboarding.

Treat the bearer token as full operator access. Requests use the agent’s normal tools and permissions, so this is not a restricted model-only API key. The Chat Completions documentation describes its fields, routing, and security boundary.

Operate the deployment from the control panel

Operate the deployment from the control panel
TaskAction
Inspect startup or errorsOpen the openclaw-gateway container logs
Run an OpenClaw commandOpen the container console and run openclaw <command>
Apply changed environment variablesRedeploy or recreate the service; a simple restart retains the old environment
Restart after a config changeUse the platform’s restart action
Upgrade OpenClawChange OPENCLAW_IMAGE to a tested exact version and redeploy
Preserve dataRetain and back up all three named volumes

The onboarding marker prevents ordinary restarts and image upgrades from overwriting the initial configuration. Changing provider variables later does not automatically rewrite the saved provider configuration. Run the appropriate onboarding or configuration command deliberately from the console when changing providers.

Back up before upgrading

The container is disposable; the named volumes are not:

Back up before upgrading
VolumeContents
openclaw-stateGateway configuration, databases, credentials, plugins, and other state
openclaw-workspaceAgent workspace and generated artifacts
openclaw-auth-profilesAuth-profile recovery key material

Use the platform’s volume snapshot, export, or backup feature and verify that it can restore all three volumes together. Stop the Gateway first when the backup mechanism does not provide an application-consistent snapshot. Copying a live SQLite database is not a reliable backup strategy.

Do not select remove volumes, delete persistent data, or an equivalent option during a redeploy. Keep the platform’s stack/project name and the three volume declarations unchanged so Compose continues to attach the same volumes.

To upgrade:

  1. Take and verify a backup.
  2. Review the newer OpenClaw release and Docker notes.
  3. Change OPENCLAW_IMAGE to a new exact tag, such as ghcr.io/openclaw/openclaw:<version>.
  4. Ask the platform to pull the image and redeploy the service.
  5. Check health, logs, openclaw doctor --json, and a small model request.

Use an exact release tag rather than a moving channel such as latest. For a reproducible artifact, pin a verified registry digest (ghcr.io/openclaw/openclaw@sha256:<digest>) when the platform supports it. A version-looking tag alone is not a cryptographic guarantee, and a digest pin must still be deliberately updated to receive fixes.

Understand the security boundary

Running the Gateway in Docker does not automatically enable OpenClaw’s agent sandbox. Agent tools can still read and change everything mounted inside the container, including the workspace. The standard prebuilt image also does not need the host Docker socket for this deployment; do not add that mount casually.

Anyone who administers the Docker host can inspect container environment variables and volumes. A managed platform therefore remains part of the trust boundary even when its UI labels values as secrets.

Before connecting untrusted senders or enabling powerful tools, read the official Gateway security and sandboxing guides. Keep direct messages in pairing or allowlist mode, approve browser devices deliberately, and expose the smallest possible workspace.

Treat one Gateway as one trust boundary for a single operator or a mutually trusting team. Separate mutually untrusted users into separate Gateways and platform projects rather than relying on prompts for tenant isolation. Each stack must have a distinct project name so its Compose-managed volumes remain separate.

With those constraints in place, the deployment is fully manageable from a Docker control panel: Compose pulls the prebuilt image, the first container start performs unattended onboarding, named volumes preserve the state, and every later administrative command runs through the container console.

Johan Boström smiling in a navy jacket and white shirt.
Author portrait

About the author

Johan Boström

Technology leader · Solution architect · Developer

My primary work is leading developers and working as a solution architect. I still develop when hands-on work helps the team or the solution.

I mainly work across systems, integrations, and AI. This archive keeps the practical details, lessons, and implementation notes worth finding again.