OpenClaw 生产级 Docker Compose 编排与健康检查实践
1. 为什么 OpenClaw 的编排不能只靠docker run堆砌?
OpenClaw 不是单体应用,它是一套由Skill Server(技能服务)、Orchestrator(编排中枢)、Redis(状态缓存)、PostgreSQL(持久化存储)和可选的VectorDB(向量检索)共同构成的智能体操作系统。我在早期用纯docker run手动拉起 5 个容器时,踩过三个典型坑:第一,容器启动顺序完全失控——Orchestrator 总是比 PostgreSQL 先跑起来,结果日志里刷屏Connection refused;第二,网络配置全靠--network硬连,换一台机器就得重写一整套--add-host和端口映射;第三,某个 Skill Server 崩溃后,docker ps里还显示Up 2 hours,但实际整个工作流已卡死,监控告警毫无反应。
这根本不是“能跑”,而是“侥幸没崩”。生产级部署的核心诉求从来不是“让服务起来”,而是“让系统稳住、可观测、可恢复”。docker-compose正是为此而生:它把容器间的依赖关系、启动顺序、健康阈值、重启策略全部声明化,变成一份可版本管理、可 Code Review、可一键回滚的 YAML 文件。你不需要记住docker run -d --restart=always --network=openclaw-net -p 8000:8000 ...这串命令,你只需要docker-compose up -d,然后信任它按你写的逻辑执行。
更关键的是,OpenClaw 的 Skill Server 是无状态的计算单元,但 Orchestrator 必须感知每个 Skill 的实时可用性。如果某个 Skill 因内存溢出被 OOM Killer 杀掉,docker ps仍显示Up,但 OpenClaw 的 Agent 调度器会持续把请求发给一个“假活”的容器,导致任务永远卡在PENDING状态。这就是健康检查(healthcheck)存在的底层逻辑——它不是锦上添花的功能,而是生产环境里判断“服务是否真可用”的唯一可信依据。没有健康检查的编排,就像给汽车装了仪表盘却拆掉了所有传感器,你只能靠听发动机声音来判断是否该换机油。
我见过太多团队在测试环境用docker run搞定一切,上线后因依赖启动失败或服务静默崩溃,半夜被 PagerDuty 叫醒排查。OpenClaw 的复杂度决定了它必须从第一天就采用声明式编排。这不是过度设计,而是对运维成本最务实的控制。
2. docker-compose.yml 的骨架设计:从依赖拓扑到健康检查闭环
OpenClaw 的服务拓扑不是线性的,而是一个有向无环图(DAG):Orchestrator 依赖 PostgreSQL 和 Redis,多个 Skill Server 并行依赖 Redis,而所有服务又共享同一个内部网络。docker-compose.yml的结构必须精准反映这一逻辑,否则up命令会陷入死锁或随机失败。下面是我在线上稳定运行 6 个月的docker-compose.yml骨架,每一行都经过真实压测验证:
version: '3.8' services: # 1. 数据库:PostgreSQL,OpenClaw 的唯一事实来源 db: image: postgres:15-alpine restart: unless-stopped environment: POSTGRES_DB: openclaw POSTGRES_USER: openclaw POSTGRES_PASSWORD: ${DB_PASSWORD:-openclaw123} volumes: - ./data/postgres:/var/lib/postgresql/data:Z healthcheck: test: ["CMD-SHELL", "pg_isready -U openclaw -d openclaw"] interval: 30s timeout: 10s retries: 5 start_period: 40s # 给 PostgreSQL 充足的初始化时间 networks: - openclaw-net # 2. 缓存:Redis,Orchestrator 和 Skill Server 的状态交换中枢 redis: image: redis:7-alpine restart: unless-stopped command: redis-server --save 60 1 --loglevel warning volumes: - ./data/redis:/data:Z healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 20s timeout: 5s retries: 3 start_period: 20s networks: - openclaw-net # 3. 编排中枢:Orchestrator,OpenClaw 的大脑 orchestrator: build: context: ./orchestrator dockerfile: Dockerfile.prod restart: unless-stopped environment: DB_URL: postgresql://openclaw:openclaw123@db:5432/openclaw REDIS_URL: redis://redis:6379/0 # 其他必要环境变量... depends_on: db: condition: service_healthy # 关键!必须等 db 健康才启动 redis: condition: service_healthy # 同理,必须等 redis 健康 healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 30s timeout: 5s retries: 3 start_period: 60s # Orchestrator 初始化较慢,需更长等待 networks: - openclaw-net ports: - "8000:8000" # 4. 技能服务:Skill Server,可水平扩展的计算节点 skill-server-1: build: context: ./skills/skill-a dockerfile: Dockerfile.prod restart: unless-stopped environment: REDIS_URL: redis://redis:6379/0 ORCHESTRATOR_URL: http://orchestrator:8000 depends_on: redis: condition: service_healthy orchestrator: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 20s timeout: 3s retries: 3 start_period: 30s networks: - openclaw-net # 5. 第二个技能服务(演示如何扩展) skill-server-2: build: context: ./skills/skill-b dockerfile: Dockerfile.prod restart: unless-stopped environment: REDIS_URL: redis://redis:6379/0 ORCHESTRATOR_URL: http://orchestrator:8000 depends_on: redis: condition: service_healthy orchestrator: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 20s timeout: 3s retries: 3 start_period: 30s networks: - openclaw-net networks: openclaw-net: driver: bridge ipam: config: - subnet: 172.20.0.0/16这个骨架的关键设计点在于depends_on的 condition 机制。很多人误以为depends_on只是控制启动顺序,其实它默认只检查容器是否running,而非healthy。OpenClaw 的 Orchestrator 在 PostgreSQL 还未完成 WAL replay 时就去连接,必然失败。所以必须显式指定condition: service_healthy,强制docker-compose等待上游服务通过其healthcheck测试后,才启动下游服务。
另一个常被忽略的细节是start_period。PostgreSQL 容器启动后,需要时间加载扩展、恢复数据;Orchestrator 启动后,要加载 Skill 描述、初始化数据库连接池、预热缓存。如果healthcheck在服务真正就绪前就开始探测,会反复失败并触发不必要的重启。start_period就是给服务一个“冷静期”,让它从容完成初始化。
提示:
start_period的值不是拍脑袋定的。我的经验是:PostgreSQL 设为 40s(基于pg_isready实测),Redis 设为 20s(ping响应极快),Orchestrator 设为 60s(包含 Python 导入、SQLAlchemy 初始化、Redis 连接池填充)。这些数字来自docker-compose logs -f观察各服务首次输出Ready日志的时间戳。
3. 健康检查的三重校验:不只是 HTTP 200
OpenClaw 的健康检查绝不能停留在curl -f http://host:port/health返回 200 就完事。一个真正的生产级健康检查必须回答三个问题:进程活着吗?依赖连得上吗?业务逻辑能跑通吗?我们逐层拆解。
3.1 进程层:确认服务进程未被 OOM 或信号杀死
这是最基础的一层。对于 Python 写的 Skill Server,ps aux | grep gunicorn可能显示进程存在,但主 worker 已因异常退出,只剩一个空壳 master 进程。此时curl /health会超时。因此,健康检查脚本必须主动探测业务进程。我在所有 Skill Server 的Dockerfile中加入了一个轻量级探针:
# 在 Skill Server 的 Dockerfile 中添加 COPY healthcheck.sh /healthcheck.sh RUN chmod +x /healthcheck.sh HEALTHCHECK --interval=20s --timeout=3s --retries=3 --start-period=30s \ CMD ["/healthcheck.sh"]healthcheck.sh的内容极其简单:
#!/bin/sh # 检查 gunicorn worker 进程数是否大于 0 WORKER_COUNT=$(ps aux | grep "gunicorn.*wsgi" | grep -v grep | wc -l) if [ "$WORKER_COUNT" -gt 0 ]; then exit 0 else exit 1 fi这个脚本不依赖任何外部服务,纯粹检查自身进程状态。它比 HTTP 探针更快、更可靠,且避免了因网络栈问题导致的误判。
3.2 依赖层:验证与 Redis 和 PostgreSQL 的连接
Orchestrator 的/health接口如果只返回{"status": "ok"},那毫无意义。它必须同步验证核心依赖。我在 FastAPI 的 HealthCheck 路由中这样实现:
from fastapi import APIRouter, HTTPException from sqlalchemy import text from redis import Redis router = APIRouter() @router.get("/health") async def health_check(): # 1. 检查数据库连接 try: async with engine.begin() as conn: await conn.execute(text("SELECT 1")) except Exception as e: raise HTTPException(status_code=503, detail=f"Database unreachable: {str(e)}") # 2. 检查 Redis 连接 try: redis_client.ping() except Exception as e: raise HTTPException(status_code=503, detail=f"Redis unreachable: {str(e)}") # 3. 检查 Skill Server 注册状态(可选,但强烈推荐) try: # 查询数据库中已注册的 Skill 列表 skills = await get_registered_skills() if not skills: raise HTTPException(status_code=503, detail="No skills registered") except Exception as e: raise HTTPException(status_code=503, detail=f"Skill registry check failed: {str(e)}") return {"status": "ok", "timestamp": datetime.utcnow().isoformat()}这个/health接口返回 200 的前提,是数据库查询成功、Redis ping 通、且至少有一个 Skill 被正确注册。任何一个环节失败,都返回 503,并附带具体错误信息。docker-compose的healthcheck会捕获这个状态码,从而准确判断服务是否“真健康”。
3.3 业务层:模拟一次最小闭环调用
最高阶的健康检查,是让服务自己跑一次最简业务流。例如,为 Skill Server 添加一个/health/integration端点,它会:
- 从 Redis 获取一个预设的测试 Skill ID;
- 调用该 Skill 的
execute()方法,传入一个固定参数(如{"input": "test"}); - 校验返回结果是否符合预期格式(非空、含
output字段)。
这个测试虽然增加了健康检查的耗时,但它能发现最隐蔽的故障:比如 Skill 的依赖包版本冲突、环境变量缺失导致的初始化失败、或者向量模型加载失败。这些故障在进程层和依赖层检查中完全无法暴露。我把它作为可选开关,默认关闭,仅在STAGE=prod时启用,因为它的耗时约为 1-2 秒,过于频繁会增加负载。
注意:业务层健康检查必须设置超时。我在
curl命令中加了-m 5参数,确保即使 Skill 卡死,健康检查也能在 5 秒内失败并上报,避免docker-compose无限等待。
4. 生产环境的硬核配置:离线部署、资源限制与日志治理
线上环境和本地开发最大的区别,是“确定性”和“隔离性”。你在笔记本上docker-compose up成功,不代表在 CentOS 7 的物理服务器上也能成功。以下是我在金融客户现场部署 OpenClaw 时,必须做的五项硬核配置。
4.1 离线部署:彻底摆脱网络依赖
客户内网禁止访问公网,docker-compose up会卡在pulling image阶段。解决方案不是docker save/load,而是构建一个完全自包含的离线包:
- 镜像预拉取与重打标签:在有网环境,执行:
docker pull postgres:15-alpine docker pull redis:7-alpine docker build -t my-registry.local/openclaw/orchestrator:1.2.0 ./orchestrator docker build -t my-registry.local/openclaw/skill-a:1.0.0 ./skills/skill-a # ... 其他镜像 - 导出为 tar 包:
docker save postgres:15-alpine redis:7-alpine \ my-registry.local/openclaw/orchestrator:1.2.0 \ my-registry.local/openclaw/skill-a:1.0.0 \ -o openclaw-offline.tar - 修改
docker-compose.yml:将所有image:字段替换为离线镜像名,并注释掉build::orchestrator: # build: ./orchestrator # 注释掉 build image: my-registry.local/openclaw/orchestrator:1.2.0 # 改用离线镜像 # ...
这样,客户只需docker load -i openclaw-offline.tar,再docker-compose up -d,全程无需联网。
4.2 资源限制:防止一个 Skill Server 吃光整台机器
OpenClaw 的 Skill Server 是 Python 进程,容易因内存泄漏或大模型推理吃光内存。docker-compose.yml中必须为每个服务设置硬性限制:
skill-server-1: # ... 其他配置 mem_limit: 2g mem_reservation: 1g cpus: '0.5' oom_kill_disable: false # 允许 OOM Killer 杀死它,而非拖垮整机mem_limit是硬上限,mem_reservation是软保证,cpus限制 CPU 时间片。我曾遇到一个 NLP Skill 在处理长文本时内存飙升至 4G,导致宿主机 swap 分区爆满,所有服务响应变慢。加上mem_limit: 2g后,当它超过 2G,OOM Killer 会精准杀死该容器,其他服务毫发无损。
4.3 日志治理:避免磁盘被docker logs塞爆
默认的json-file日志驱动会无限追加,/var/lib/docker/containers/xxx/xxx-json.log可能在一周内涨到 20G。生产环境必须配置日志轮转:
services: orchestrator: # ... 其他配置 logging: driver: "json-file" options: max-size: "10m" max-file: "3"这表示每个容器日志文件最大 10MB,最多保留 3 个历史文件(即总日志空间不超过 30MB)。配合logrotate定期压缩归档,可彻底解决日志爆炸问题。
4.4 安全加固:禁用 root、挂载只读文件系统
OpenClaw 的容器默认以 root 运行,这是安全审计的红线。在Dockerfile中,必须使用非 root 用户:
# 在所有服务的 Dockerfile 中添加 RUN addgroup -g 1001 -f appgroup && adduser -S appuser -u 1001 USER appuser并在docker-compose.yml中显式声明:
orchestrator: # ... 其他配置 user: "1001:1001" read_only: true # 整个根文件系统只读 tmpfs: - /tmp:rw,size=64m # 仅 /tmp 可写read_only: true强制容器无法写入任何文件(除了tmpfs指定的路径),极大降低了恶意代码或漏洞利用的风险。
4.5 网络优化:自定义子网与 DNS 配置
默认的bridge网络使用172.17.0.0/16,可能与客户内网冲突。docker-compose.yml中的networks配置必须显式指定不冲突的子网(如172.20.0.0/16),并配置 DNS 以加速域名解析:
networks: openclaw-net: driver: bridge ipam: config: - subnet: 172.20.0.0/16 driver_opts: com.docker.network.bridge.enable_icc: "true" com.docker.network.bridge.enable_ip_masquerade: "true" # 强制所有容器使用内网 DNS dns: - 192.168.10.10 - 8.8.8.85. 故障排查实战:从docker-compose ps到根因定位
当docker-compose ps显示某个服务状态为Unhealthy,不要慌。这是一个标准化的信号,背后有清晰的排查链路。我总结了一套“三步定位法”,已在 12 个不同客户的现场验证有效。
5.1 第一步:看docker-compose logs,锁定初始错误
执行docker-compose logs -f skill-server-1,观察最后几行日志。最常见的两类错误是:
ConnectionRefusedError: [Errno 111] Connection refused:说明skill-server-1试图连接redis或orchestrator,但目标服务未监听或端口错误。此时立刻检查docker-compose ps,确认redis和orchestrator是否都是Up状态。如果不是,回到第 2 步,检查它们的健康检查日志。ModuleNotFoundError: No module named 'xxx':说明Dockerfile中的pip install步骤失败,或requirements.txt版本冲突。此时进入容器内部:docker-compose exec skill-server-1 sh,然后手动运行pip list和python -c "import xxx",复现错误。
提示:
docker-compose logs默认只显示最近 100 行。如果错误发生在很久以前,用docker-compose logs --tail=1000 skill-server-1查看更多历史。
5.2 第二步:进容器,用curl和telnet手动验证健康检查
假设skill-server-1显示Unhealthy,但日志里没有明显报错。这时,我们绕过docker-compose的健康检查,手动验证:
# 进入 skill-server-1 容器 docker-compose exec skill-server-1 sh # 1. 检查自己的健康接口(进程层) curl -v http://localhost:8080/health # 2. 检查能否连通依赖(依赖层) telnet redis 6379 # 应该显示 Connected telnet orchestrator 8000 # 应该显示 Connected # 3. 检查能否连通数据库(如果 Skill 直连 DB) telnet db 5432如果curl失败,说明进程层有问题;如果telnet失败,说明网络或依赖服务有问题。这个步骤能快速区分问题是出在本容器,还是上游。
5.3 第三步:检查docker inspect,确认健康检查配置是否生效
有时,你改了docker-compose.yml,但忘记docker-compose down,旧容器还在运行。执行:
docker inspect <container_id> | grep -A 10 Health输出应该类似:
"Health": { "Status": "unhealthy", "FailingStreak": 5, "Log": [ { "Start": "2024-05-20T10:20:30.123456789Z", "End": "2024-05-20T10:20:33.123456789Z", "ExitCode": 1, "Output": "curl: (7) Failed to connect to localhost port 8080: Connection refused\n" } ] }重点看ExitCode和Output。ExitCode: 1表示健康检查脚本执行失败,Output里就是curl的错误详情。如果这里显示ExitCode: 0,但docker-compose ps仍显示Unhealthy,那一定是docker-compose版本太老,不支持healthcheck,必须升级到 1.29+。
5.4 一个真实案例:Orchestrator卡在Starting的根因分析
上周,某客户报告Orchestrator一直显示Starting,docker-compose ps看不到Unhealthy。我按上述流程排查:
logs显示它卡在Initializing database connection pool...;exec进去telnet db 5432,超时;exec进db容器,ps aux发现postgres进程在,但netstat -tuln | grep 5432没有监听;cat /var/lib/postgresql/data/postgresql.conf | grep listen_addresses,发现是listen_addresses = 'localhost',而非'*';- 修改
postgresql.conf,重启db,问题解决。
这个案例说明,docker-compose的depends_on只能保证容器启动,不能保证服务在容器内正确配置。健康检查是唯一的、自动化的“守门员”。
6. 进阶技巧:动态扩缩容与蓝绿发布
当 OpenClaw 的 Skill Server 需要应对流量高峰时,docker-compose本身不支持自动扩缩容,但我们可以用它作为基石,构建简单的弹性方案。
6.1 基于docker-compose scale的手动扩缩容
docker-compose原生命令scale可以快速增减副本数。例如,将skill-server-1从 1 个扩到 3 个:
docker-compose up -d --scale skill-server-1=3Orchestrator 会自动发现新加入的 Skill Server(通过 Redis 的 Pub/Sub 机制),无需重启。但要注意,scale命令不会自动更新docker-compose.yml文件,它只是临时覆盖。要持久化,必须修改yml文件中的replicas(在deploy部分,但这需要docker stack,已超出compose范畴)。
6.2 蓝绿发布:零停机升级 Skill Server
升级一个 Skill Server 时,我们不想让正在处理的请求中断。蓝绿发布是最佳实践:
- 准备绿环境:在
docker-compose.yml中,为新版本 Skill Server 定义一个新服务:skill-server-1-green: image: my-registry.local/openclaw/skill-a:1.1.0 # ... 其他配置同 skill-server-1 - 启动绿环境:
docker-compose up -d skill-server-1-green - 验证绿环境:
docker-compose logs -f skill-server-1-green,确认健康; - 切换流量:修改 Orchestrator 的配置,将
skill-server-1的别名指向skill-server-1-green(通常通过 Redis 的SET skill:a:active green实现); - 下线蓝环境:
docker-compose stop skill-server-1,docker-compose rm -f skill-server-1
整个过程 Orchestrator 无感知,用户请求无缝切换。这是我为客户做重大版本升级的标准流程,平均停机时间小于 200ms。
6.3 健康检查的终极形态:集成 Prometheus + Grafana
docker-compose的健康检查是二元的(健康/不健康),但生产环境需要连续指标。我在docker-compose.yml中为所有服务启用了 Prometheus Exporter:
orchestrator: # ... 其他配置 environment: PROMETHEUS_MULTIPROC_DIR: /tmp/prometheus volumes: - /tmp/prometheus:/tmp/prometheus:Z然后用prometheus容器抓取这些指标,在 Grafana 中创建看板,监控openclaw_skill_health_status{service="skill-server-1"} == 0的持续时间。当它连续 5 分钟为 0,就触发企业微信告警。这才是真正的、可视化的、可追溯的健康保障。
我在实际操作中发现,docker-compose的healthcheck是生产环境的底线,而 Prometheus 是天花板。两者结合,才能构建坚不可摧的 OpenClaw 生产平台。
