一、准备

环境假设:

  • Ubuntu / Debian VPS
  • 已安装 Docker 和 Docker Compose v2
  • 宿主机已安装 Caddy
  • 域名 A/AAAA 记录已指向 VPS,80/443 已放行

目录:

mkdir -p ~/openclaw
cd ~/openclaw

二、docker-compose.yml

services:
  openclaw-gateway:
    image: ${OPENCLAW_IMAGE:-ghcr.io/openclaw/openclaw:latest}
    container_name: openclaw-gateway
    env_file: .env
    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_TOKEN: ${OPENCLAW_GATEWAY_TOKEN:-}
      TZ: ${OPENCLAW_TZ:-Asia/Shanghai}
    volumes:
      - ./data/config:/home/node/.openclaw
      - ./data/workspace:/home/node/.openclaw/workspace
      - ./data/auth-profile-secrets:/home/node/.config/openclaw
    ports:
      - "127.0.0.1:18789:18789"
    cap_drop:
      - NET_RAW
      - NET_ADMIN
    security_opt:
      - no-new-privileges:true
    extra_hosts:
      - "host.docker.internal:host-gateway"
    init: true
    restart: unless-stopped
    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: 20s

  openclaw-cli:
    profiles: ["cli"]
    image: ${OPENCLAW_IMAGE:-ghcr.io/openclaw/openclaw:latest}
    network_mode: "service:openclaw-gateway"
    env_file: .env
    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_TOKEN: ${OPENCLAW_GATEWAY_TOKEN:-}
      TZ: ${OPENCLAW_TZ:-Asia/Shanghai}
      BROWSER: echo
    volumes:
      - ./data/config:/home/node/.openclaw
      - ./data/workspace:/home/node/.openclaw/workspace
      - ./data/auth-profile-secrets:/home/node/.config/openclaw
    stdin_open: true
    tty: true
    init: true
    entrypoint: ["node", "dist/index.js"]
    depends_on:
      - openclaw-gateway

几个刻意的选择:

  • ports 写成 127.0.0.1:18789:18789,不是 18789:18789
  • 容器内 --bind lan,否则 Docker 端口映射进不来
  • openclaw-cli 用 profiles 收起,平时不启动
  • cap_drop 和 no-new-privileges 沿用官方 compose 的加固

三、.env

DOMAIN=openclaw.example.com
OPENCLAW_GATEWAY_TOKEN=用 openssl rand -hex 32 生成
OPENCLAW_IMAGE=ghcr.io/openclaw/openclaw:latest
OPENCLAW_TZ=Asia/Shanghai

# 可选
#ANTHROPIC_API_KEY=sk-ant-xxxx

生成 Token:

openssl rand -hex 32

数据目录权限:

mkdir -p data/config data/workspace data/auth-profile-secrets
sudo chown -R 1000:1000 data

容器内以 node 用户运行,通常是 UID 1000。权限不对时,容器会以 EACCES 反复重启。

四、初始化

拉镜像:

docker compose pull

非交互式 onboarding,以 Anthropic 为例:

set -a; . ./.env; set +a
docker compose run --rm --no-deps --entrypoint node openclaw-gateway dist/index.js onboard \
  --non-interactive --accept-risk --skip-health --mode local \
  --auth-choice apiKey \
  --anthropic-api-key "$ANTHROPIC_API_KEY" \
  --secret-input-mode ref \
  --gateway-auth token \
  --gateway-token-ref-env OPENCLAW_GATEWAY_TOKEN \
  --skip-channels --no-install-daemon

如果暂时不配置模型,把 provider 参数换成:

--auth-choice skip

然后写入公网地址和代理信任:

docker compose run --rm --no-deps --entrypoint node openclaw-gateway dist/index.js config set --batch-json "$(printf '[{"path":"gateway.mode","value":"local"},{"path":"gateway.bind","value":"lan"},{"path":"gateway.publicOrigin","value":"https://%s"},{"path":"gateway.controlUi.allowedOrigins","value":["https://%s"]},{"path":"gateway.trustedProxies","value":["127.0.0.1","::1","172.16.0.0/12"]}]' "$DOMAIN" "$DOMAIN")"

trustedProxies 这里用 172.16.0.0/12 覆盖 Docker 默认私网段。因为 18789 只绑回环,这个范围是可控的;想更严格就换成实际网桥网关地址。

启动:

docker compose up -d
curl -fsS http://127.0.0.1:18789/healthz

五、Caddy 配置

标准情况:Caddy 直接监听 443

openclaw.example.com {
    encode zstd gzip

    reverse_proxy 127.0.0.1:18789 {
        flush_interval -1
    }
}

新的情况:443 → 8443 的 layer4 端口复用

Caddy 全局用了 https_port 8443,再在 :443 上用 layer4 按 SNI 分流:Reality、Naive 各走各的,剩下的 HTTPS 流量转发到本机 8443。

关键配置:

{
    http_port 80
    https_port 8443

    servers {
        listener_wrappers {
            proxy_protocol {
                timeout 5s
                allow 127.0.0.1/32 ::1/128
                fallback_policy ignore
            }
            tls
        }
    }

    layer4 {
        :443 {
            @reality tls sni reality.example.com
            route @reality {
                proxy 127.0.0.1:10118
            }

            @naive tls sni naive.example.com
            route @naive {
                proxy 127.0.0.1:10115
            }

            route {
                proxy 127.0.0.1:8443 {
                    proxy_protocol v2
                }
            }
        }
    }
}

:80 {
    root * /usr/share/caddy
    file_server
}

openclaw.example.com {
    encode zstd gzip

    reverse_proxy 127.0.0.1:18789 {
        flush_interval -1
    }
}

校验并重载:

sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

六、首次登录

打开 https://openclaw.example.com/,在 Settings 里粘贴 .env 中的 OPENCLAW_GATEWAY_TOKEN。

如果页面提示等待设备批准:

cd ~/openclaw && docker compose exec openclaw-gateway node dist/index.js devices list
cd ~/openclaw && docker compose exec openclaw-gateway node dist/index.js devices approve <requestId>

注意:宿主机上没有 openclaw 命令是正常的,CLI 装在容器里。所有命令都要通过 docker compose exec / docker compose run 执行。

七、安全与维护

  • 18789 始终只绑 127.0.0.1
  • 容器保留 cap_drop 和 no-new-privileges
  • OPENCLAW_GATEWAY_TOKEN 用 64 位十六进制随机串
  • 生产环境固定镜像版本,不要长期用 latest
  • 定期备份 data/、.env 和 Caddyfile

升级:

cd ~/openclaw && docker compose pull && docker compose up -d

备份:

cd ~/openclaw && tar czf openclaw-backup-$(date +%F).tar.gz data .env docker-compose.yml