NAS 上用 Docker 部署 OpenClaw

在 NAS 的 Docker 里跑 OpenClaw 网关:初始化配置、持久化状态目录、docker compose 编排,以及内网访问时该开哪些开关、不该开哪些。

NAS 常年开机、盘位充足,很适合当常驻服务的宿主。OpenClaw 网关就是一个典型的常驻进程:它需要持久化的状态目录、一个稳定的端口,以及重启后能自己回来的编排方式。

下面这套是单容器的最小可用配置,跑在群晖/威联通这类带 Docker 的 NAS 上。

一、先跑一次初始化

首次启动要先跑 onboard 生成配置,跑完容器退出,配置留在挂载出来的目录里。

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='<随机令牌>' \
  -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

三个要点:

  • --user 1000:1000 要和宿主机目录的属主对齐,否则容器写不进挂载点。群晖的普通用户通常是 1026,用 id 查一下再改。
  • OPENCLAW_GATEWAY_TOKEN 不要留空。空值等于没有鉴权,任何能碰到这个端口的人都能接管你的网关。用 openssl rand -hex 32 生成一个。
  • 两个卷要分开挂:/home/node/.openclaw 是状态和 workspace,/home/node/.config/openclaw 是 auth profile 密钥。混在一起的话,备份状态目录时会连密钥一起带走。

二、配置文件里的网关段

初始化完,openclaw.json 里至少要有这一段:

{
  "gateway": {
    "mode": "local",
    "auth": {
      "mode": "token",
      "token": "<与上面 OPENCLAW_GATEWAY_TOKEN 相同的值>"
    },
    "port": 18789,
    "bind": "lan",
    "tailscale": {
      "mode": "off",
      "resetOnExit": false
    }
  }
}

bind: "lan" 是为了让同网段的设备能连上 Control UI。如果只在 NAS 本机用,改成 "loopback" 更安全。

三、关于两个「危险开关」

网上不少教程会建议在内网部署时加上这两行,让手机、平板不用配对就能登录:

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

它们的实际效果是:绕过设备配对,允许非 HTTPS 来源认证。只有在完全隔离的内网、且你清楚谁在用这个网关时才开。一旦端口被映射到公网、或者 NAS 上跑了反向代理把它暴露出去,这两项就等于把门锁拆了。默认配置里不要带。

我的建议是:先按默认(不带这两项)部署,用手机走一次正常的设备配对流程。配对是一次性的,配完就不用再管,没必要为此长期削弱鉴权。

四、docker compose 编排

初始化跑通之后,用 compose 接管日常启动:

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
      # 容器内路径固定,避免宿主机路径泄漏进运行时
      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}
      # 可选:模型 Key(也可以只写进 openclaw.json)
      OPENAI_API_KEY: ${OPENAI_API_KEY:-}
      ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}

    volumes:
      # 状态 + 配置 + workspace,全部持久化到 NAS
      - ${OPENCLAW_DATA_DIR:-/vol1/1000/docker/openclaw}:/home/node/.openclaw
      # auth profile 密钥目录,别和状态目录混在一起
      - ${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(可选)
      # - "3978:3978"                             # 只有接 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

cap_drop 和 no-new-privileges 这两项建议保留。网关本身不需要 NET_RAW/NET_ADMIN,去掉能少一个被利用的面。start_period: 60s 是给首次初始化留的余量,老 NAS 的盘慢,别设太短。

五、验证

起来之后先确认容器健康,再确认端口只在预期的网段可达:

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

返回 200 说明 Control UI 已经能访问。然后从路由器层面确认 18789 没有做公网端口映射——这是这套部署里最关键的一步。