Running OpenClaw on a NAS with Docker

Deploy the OpenClaw gateway inside Docker on a NAS: bootstrap the config, persist state directories, orchestrate with compose, and know which auth switches to leave alone.

A NAS is always on and has plenty of disk, which makes it a good host for long-running services. The OpenClaw gateway is exactly that kind of process: it wants a persistent state directory, a stable port, and an orchestration setup that brings it back after a reboot.

What follows is a minimal single-container configuration for Synology/QNAP-class NAS boxes with Docker.

1. Bootstrap once

The first run executes onboard to generate the config. The container exits afterwards, and the config stays in the mounted directory.

docker run -it --rm \
  --user 1000:1000 \
  -e HOME=/home/node \
  -e OPENCLAW_STATE_DIR=/home/node/.openclaw \
  -e OPENCLAW_CONFIG_PATH=/home/node/.openclaw/openclaw.json \
  -e OPENCLAW_GATEWAY_TOKEN='<random-token>' \
  -v /vol1/1000/docker/openclaw:/home/node/.openclaw \
  -v /vol1/1000/docker/openclaw-auth:/home/node/.config/openclaw \
  alpine/openclaw \
  node dist/index.js onboard --mode local --no-install-daemon

Three things to get right:

  • --user 1000:1000 must match the owner of the host directory, or the container cannot write to the mount. Synology’s regular users are often 1026; check with id and adjust.
  • Do not leave OPENCLAW_GATEWAY_TOKEN empty. An empty value means no authentication, and anyone who can reach the port can take over your gateway. Generate one with openssl rand -hex 32.
  • Keep the two volumes separate: /home/node/.openclaw holds state and workspace, /home/node/.config/openclaw holds auth profile keys. Mixing them means backing up the state directory also copies your keys.

2. The gateway section in the config

After onboarding, openclaw.json should contain at least this:

{
  "gateway": {
    "mode": "local",
    "auth": {
      "mode": "token",
      "token": "<same value as OPENCLAW_GATEWAY_TOKEN above>"
    },
    "port": 18789,
    "bind": "lan",
    "tailscale": {
      "mode": "off",
      "resetOnExit": false
    }
  }
}

bind: "lan" lets devices on the same subnet reach the Control UI. If you only use the gateway on the NAS itself, "loopback" is safer.

3. About those two “dangerous” switches

Plenty of tutorials suggest adding these two lines for LAN deployments so phones and tablets can sign in without pairing:

{
  "gateway": {
    "controlUi": {
      "allowInsecureAuth": true,
      "dangerouslyDisableDeviceAuth": true
    }
  }
}

What they actually do: bypass device pairing and allow authentication from non-HTTPS origins. Enable them only on a fully isolated LAN where you know exactly who uses the gateway. The moment the port is mapped to the internet, or a reverse proxy on the NAS exposes it, those two lines amount to removing the door lock. Leave them out of your default config.

My recommendation: deploy with the defaults (no such switches) and run the normal device pairing flow once from your phone. Pairing is a one-time step, so there is no reason to permanently weaken authentication for it.

4. Compose orchestration

Once onboarding works, let compose own the day-to-day startup:

services:
  openclaw-gateway:
    image: ${OPENCLAW_IMAGE:-alpine/openclaw}
    container_name: openclaw-gateway
    restart: unless-stopped
    init: true

    environment:
      HOME: /home/node
      OPENCLAW_HOME: /home/node
      # Fixed in-container paths, so host paths never leak into the runtime
      OPENCLAW_STATE_DIR: /home/node/.openclaw
      OPENCLAW_CONFIG_DIR: /home/node/.openclaw
      OPENCLAW_CONFIG_PATH: /home/node/.openclaw/openclaw.json
      OPENCLAW_WORKSPACE_DIR: /home/node/.openclaw/workspace
      OPENCLAW_GATEWAY_PORT: "18789"
      TZ: ${TZ:-Asia/Shanghai}
      # Optional: model keys (or keep them in openclaw.json only)
      OPENAI_API_KEY: ${OPENAI_API_KEY:-}
      ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}

    volumes:
      # State + config + workspace, all persisted to the NAS
      - ${OPENCLAW_DATA_DIR:-/vol1/1000/docker/openclaw}:/home/node/.openclaw
      # Auth profile keys, deliberately kept out of the state directory
      - ${OPENCLAW_AUTH_DIR:-/vol1/1000/docker/openclaw-auth}:/home/node/.config/openclaw

    ports:
      - "${OPENCLAW_GATEWAY_PORT:-18789}:18789"   # Control UI + Gateway API
      - "${OPENCLAW_BRIDGE_PORT:-18790}:18790"    # Bridge (optional)
      # - "3978:3978"                             # only for MS Teams

    cap_drop:
      - NET_RAW
      - NET_ADMIN
    security_opt:
      - no-new-privileges:true
    extra_hosts:
      - "host.docker.internal:host-gateway"

    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: 60s

Keep cap_drop and no-new-privileges. The gateway needs neither NET_RAW nor NET_ADMIN, so dropping them removes one more attack surface. start_period: 60s allows for slow NAS disks during first initialization — do not set it too low.

5. Verify

Once it is up, confirm the container is healthy and the port is reachable only where you expect:

docker compose ps
curl -s http://<NAS-LAN-IP>:18789/ -o /dev/null -w "%{http_code}\n"

A 200 means the Control UI is reachable. Then confirm at the router that port 18789 has no public mapping — that is the single most important step in this whole deployment.