OpenClaw 生产级 Docker Compose 编排与健康检查实践

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 --restartalways --networkopenclaw-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 的服务拓扑不是线性的而是一个有向无环图DAGOrchestrator 依赖 PostgreSQL 和 Redis多个 Skill Server 并行依赖 Redis而所有服务又共享同一个内部网络。docker-compose.yml的结构必须精准反映这一逻辑否则up命令会陷入死锁或随机失败。下面是我在线上稳定运行 6 个月的docker-compose.yml骨架每一行都经过真实压测验证version: 3.8 services: # 1. 数据库PostgreSQLOpenClaw 的唯一事实来源 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. 缓存RedisOrchestrator 和 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. 编排中枢OrchestratorOpenClaw 的大脑 orchestrator: build: context: ./orchestrator dockerfile: Dockerfile.prod restart: unless-stopped environment: DB_URL: postgresql://openclaw:openclaw123db: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 设为 20sping响应极快Orchestrator 设为 60s包含 Python 导入、SQLAlchemy 初始化、Redis 连接池填充。这些数字来自docker-compose logs -f观察各服务首次输出Ready日志的时间戳。3. 健康检查的三重校验不只是 HTTP 200OpenClaw 的健康检查绝不能停留在curl -f http://host:port/health返回 200 就完事。一个真正的生产级健康检查必须回答三个问题进程活着吗依赖连得上吗业务逻辑能跑通吗我们逐层拆解。3.1 进程层确认服务进程未被 OOM 或信号杀死这是最基础的一层。对于 Python 写的 Skill Serverps aux | grep gunicorn可能显示进程存在但主 worker 已因异常退出只剩一个空壳 master 进程。此时curl /health会超时。因此健康检查脚本必须主动探测业务进程。我在所有 Skill Server 的Dockerfile中加入了一个轻量级探针# 在 Skill Server 的 Dockerfile 中添加 COPY healthcheck.sh /healthcheck.sh RUN chmod x /healthcheck.sh HEALTHCHECK --interval20s --timeout3s --retries3 --start-period30s \ 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_code503, detailfDatabase unreachable: {str(e)}) # 2. 检查 Redis 连接 try: redis_client.ping() except Exception as e: raise HTTPException(status_code503, detailfRedis unreachable: {str(e)}) # 3. 检查 Skill Server 注册状态可选但强烈推荐 try: # 查询数据库中已注册的 Skill 列表 skills await get_registered_skills() if not skills: raise HTTPException(status_code503, detailNo skills registered) except Exception as e: raise HTTPException(status_code503, detailfSkill 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 的依赖包版本冲突、环境变量缺失导致的初始化失败、或者向量模型加载失败。这些故障在进程层和依赖层检查中完全无法暴露。我把它作为可选开关默认关闭仅在STAGEprod时启用因为它的耗时约为 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后当它超过 2GOOM 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,size64m # 仅 /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 --tail1000 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一直显示Startingdocker-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-13Orchestrator 会自动发现新加入的 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-1docker-compose rm -f skill-server-1整个过程 Orchestrator 无感知用户请求无缝切换。这是我为客户做重大版本升级的标准流程平均停机时间小于 200ms。6.3 健康检查的终极形态集成 Prometheus Grafanadocker-compose的健康检查是二元的健康/不健康但生产环境需要连续指标。我在docker-compose.yml中为所有服务启用了 Prometheus Exporterorchestrator: # ... 其他配置 environment: PROMETHEUS_MULTIPROC_DIR: /tmp/prometheus volumes: - /tmp/prometheus:/tmp/prometheus:Z然后用prometheus容器抓取这些指标在 Grafana 中创建看板监控openclaw_skill_health_status{serviceskill-server-1} 0的持续时间。当它连续 5 分钟为 0就触发企业微信告警。这才是真正的、可视化的、可追溯的健康保障。我在实际操作中发现docker-compose的healthcheck是生产环境的底线而 Prometheus 是天花板。两者结合才能构建坚不可摧的 OpenClaw 生产平台。