OpenClaw云容器化部署:技能引擎的生产级落地实践
1. 这不是又一个“一键部署”幻觉OpenClaw在云容器里到底要解决什么真实问题你搜“openclaw安装教程”页面刷出来几十个标题党——“三分钟跑通”“保姆级教学”“小白秒变大神”。我点开三个两个卡在openclaw : 无法将“openclaw”项识别为 cmdlet、函数、脚本文件或可运行程序的名称这行报错上第三个干脆把pip install openclaw当成万能解药结果装完连openclaw --help都打不出响应。这不是教程这是行为艺术。OpenClaw本身不是个“开箱即用”的应用它是个技能执行引擎Skill Execution Engine核心价值在于把一堆零散的API调用、命令行工具、Python脚本、甚至本地CLI程序封装成统一语义的“技能Skill”再通过自然语言指令触发执行。比如你说“把今天钉钉群里的会议纪要转成Markdown发到飞书文档”背后可能调用钉钉Webhook、调用飞书Bot API、调用LLM做格式转换——OpenClaw不负责写这些逻辑它只负责把它们串起来、管起来、安全地跑起来。而“云容器部署”这个动作恰恰是它落地最关键的临门一脚。为什么因为OpenClaw的Skill本质是代码片段有依赖、有环境、有权限、有状态。你在自己笔记本上pip install成功不代表它能在生产环境稳定运行。你本地装了ffmpeg但服务器没装video_compress这个Skill就直接哑火你本地用个人Token调用百炼API但团队协作时Token硬编码在配置里等于把钥匙贴在门框上你本地跑得好好的一上服务器就报Permission denied: /root/.openclaw——这些都不是Bug是环境失配的必然结果。所以“云容器部署OpenClaw”这件事本质是把一个动态、多依赖、强上下文的技能执行系统变成一个可版本化、可复现、可隔离、可伸缩的标准化服务单元。它解决的不是“能不能跑”而是“能不能稳、能不能管、能不能扩、能不能换人接手”。百炼Token Plan的接入就是这个闭环里最关键的一环它不是简单填个API Key而是要把Token的生命周期管理、调用配额控制、错误降级策略、审计日志追踪全部纳入OpenClaw的技能调度链路里。没有这一步OpenClaw顶多是个高级版的bash脚本集合有了它才真正具备企业级自动化中枢的雏形。这也是为什么本篇不叫“OpenClaw安装指南”而叫“全流程配置指南”——安装只是5分钟的事配置才是50小时的活。接下来每一节我都不会告诉你“点这里、输那里”而是带你搞清楚为什么必须用Docker Compose而不是单个docker run为什么Skill目录结构必须这样组织为什么百炼Token不能写死在.env里为什么健康检查探针要同时检查HTTP端口和技能加载状态这些决定决定了你搭出来的是一套玩具还是一套能扛住业务流量、经得起审计、换人也能快速上手的生产级能力平台。2. 容器化不是套壳从零构建OpenClaw镜像的底层逻辑与避坑实录很多人以为容器化就是Dockerfile里写个FROM python:3.11再RUN pip install openclaw最后CMD [openclaw, serve]——这确实能跑起来但离“可用”差了十万八千里。我试过这种最简方案上线第三天就崩了一个用户上传了带中文路径的PDFOpenClaw在解析时触发了UnicodeDecodeError整个服务进程直接退出没有任何日志监控告警全哑。查了一晚上发现是基础镜像python:3.11-slim里缺了locales包导致Python默认编码不是UTF-8。这种坑只有亲手踩过、亲手修过才能刻进DNA。所以构建OpenClaw镜像核心原则就一条让容器环境无限逼近一个干净、完整、可预测的Linux开发机。不是越小越好而是“够用且确定”。2.1 基础镜像选型为什么放弃alpine坚定选择debian:slim网络上很多教程推荐python:3.11-alpine理由很朴素体积小。确实alpine镜像只有50MB出头而python:3.11-slim基于debian是120MB。但代价是什么是musl libc和glibc的ABI不兼容。OpenClaw的Skill生态里大量依赖pandas、numpy、opencv-python-headless这类C扩展库它们在alpine上编译极其痛苦要么得自己配交叉编译链要么得用--no-binary强制源码编译耗时动辄半小时CI/CD流水线直接卡死。更致命的是百炼SDK底层依赖requests和urllib3而urllib3在musl环境下对某些SSL证书链的处理有微妙差异会导致偶发性连接超时错误日志里只显示ConnectionResetError根本看不出和SSL有关。我为此排查了两天最后抓包对比才发现是证书验证环节被musl跳过了。所以我最终锁定python:3.11-slim-bookwormdebian 12。它体积虽大但全面兼容glibc生态所有PyPI包pip install开箱即用locales包预装LANGC.UTF-8开箱即用彻底规避中文路径/文件名乱码curl、wget、jq、ca-certificates等运维常用工具一应俱全方便调试社区支持度高遇到问题Google一搜就是解决方案不用自己造轮子。提示不要用python:3.11不带slim它包含大量开发工具gcc, make等会把镜像体积拉到900MB毫无必要。slim版本是生产环境的黄金平衡点。2.2 Dockerfile逐行拆解每一行背后的“为什么”下面是我线上稳定运行半年的Dockerfile不是模板是血泪经验# syntaxdocker/dockerfile:1 FROM python:3.11-slim-bookworm # 1. 设置时区和语言环境 —— 这是所有时间戳、日志、文件名的基础 ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezone ENV LANGC.UTF-8 ENV LC_ALLC.UTF-8 # 2. 创建非root用户 —— OpenClaw官方明确要求禁止root运行 RUN groupadd -g 1001 -r openclaw useradd -r -u 1001 -g openclaw openclaw USER openclaw # 3. 创建工作目录并设置权限 —— 避免后续RUN命令以root身份创建文件导致权限混乱 WORKDIR /app RUN mkdir -p /app/skills /app/config /app/logs RUN chown -R openclaw:openclaw /app # 4. 复制依赖清单并安装系统级依赖 —— 关键很多Skill需要libjpeg、libpng等 COPY --chownopenclaw:openclaw requirements.txt . RUN apt-get update apt-get install -y --no-install-recommends \ libjpeg-dev \ libpng-dev \ libtiff-dev \ libwebp-dev \ ffmpeg \ curl \ jq \ ca-certificates \ rm -rf /var/lib/apt/lists/* # 5. 安装Python依赖 —— 使用--no-cache-dir避免镜像层膨胀 RUN pip install --no-cache-dir --upgrade pip setuptools wheel RUN pip install --no-cache-dir -r requirements.txt # 6. 复制应用代码和配置骨架 —— 注意此时不复制实际Skill代码留给运行时挂载 COPY --chownopenclaw:openclaw . . # 7. 暴露端口和健康检查 —— 健康检查必须能反映真实服务状态 EXPOSE 8000 HEALTHCHECK --interval30s --timeout3s --start-period5s --retries3 \ CMD curl -f http://localhost:8000/health || exit 1 # 8. 启动命令 —— 使用gunicorn而非默认的uvicorn为生产负载兜底 CMD [gunicorn, --bind, 0.0.0.0:8000, --workers, 2, --worker-class, sync, --timeout, 120, --log-level, info, openclaw.app:app]关键点解释第2行USER openclawOpenClaw官方文档白纸黑字写着“Running as root is not supported and will cause the application to fail”。很多教程忽略这点导致容器启动后立即崩溃报错PermissionError: [Errno 13] Permission denied: /root/.openclaw。USER指令必须放在WORKDIR和RUN之后否则后续RUN命令会以root身份执行创建的文件属主还是root普通用户进不去。第4行系统依赖libjpeg-dev等不是可选是PillowOpenClaw处理图片Skill必备的编译依赖。ffmpeg是视频处理Skill的刚需。ca-certificates确保HTTPS请求尤其是调用百炼API能正确验证证书链否则会出现SSLError: certificate verify failed。第6行COPY . .这里只复制openclaw的源码和pyproject.toml等元数据绝不复制skills/目录下的实际Skill代码。原因很简单Skill代码是业务逻辑需要频繁更新、灰度发布、A/B测试。如果打包进镜像每次改一行Skill就得重新构建、推送、拉取整个镜像CI/CD效率归零。正确的做法是通过Docker Volume或Bind Mount在容器启动时动态挂载。第8行CMDgunicorn比uvicorn更适合生产。uvicorn是异步ASGI服务器单Worker性能高但一旦某个Skill执行阻塞比如调用一个慢API整个Event Loop就被卡死。gunicorn的多Worker模型天然隔离一个Worker挂了不影响其他。参数--timeout 120是给百炼API调用留足缓冲避免因网络抖动误杀进程。2.3 构建与验证如何确认你的镜像真的“干净”构建命令不是docker build -t my-openclaw .就完事。必须加两道验证启动后检查进程树docker run -d --name test-openclaw my-openclaw docker exec test-openclaw ps aux正确输出应该只看到gunicorn: master和gunicorn: worker进程且UID都是1001即openclaw用户。如果看到root进程说明USER指令没生效或位置错了。检查依赖完整性docker exec test-openclaw python -c import openclaw; print(openclaw.__version__) docker exec test-openclaw python -c import requests; print(requests.__version__) docker exec test-openclaw ffmpeg -version | head -n1这三行必须全部成功。特别是ffmpeg很多教程只测Python依赖忘了验证系统级二进制是否真在PATH里。我见过太多人卡在这一步镜像构建成功docker run也返回Started但一调API就500。进去一看ps aux里全是root进程ffmpeg命令不存在——表面看是“部署成功”实际是“虚假繁荣”。容器化的第一课永远是“所见即所得”镜像里有什么运行时就必须有什么不多不少不偏不倚。3. 百炼Token Plan不是填空题Token生命周期管理与安全接入的硬核实践“配置百炼Token”这五个字是OpenClaw部署里含金量最高的环节。网上90%的教程到这里就变成“打开百炼控制台 → 复制API Key → 粘贴到.env文件的BAI_LIAN_TOKEN后面 → 重启服务”。然后呢然后就没有然后了。用户问“Token过期了怎么办”回答是“重新复制粘贴”。这哪是配置这是手动续费。真正的Token Plan是一套完整的凭证生命周期管理体系Credential Lifecycle Management。它必须覆盖获取、存储、注入、刷新、轮换、审计、失效处理。OpenClaw本身不提供这套体系它只提供一个BAI_LIAN_TOKEN环境变量的读取接口。剩下的全靠你设计。3.1 为什么绝对不能把Token写死在.env或Dockerfile里这是安全红线没有商量余地。原因有三Git泄露风险.env文件一旦被误提交到Git仓库哪怕只是本地分支Token就等于公开。百炼Token不像GitHub Token有细粒度权限它默认拥有你账号下所有模型的调用权。去年就有团队因此被恶意调用千次qwen-max账单直接破万。镜像层固化如果在Dockerfile里ENV BAI_LIAN_TOKENxxx这个Token会成为镜像的一个不可变Layer。你想换Token必须重新构建、推送、拉取整个镜像。一次Token轮换服务停机十分钟。缺乏审计线索Token谁在用、什么时候用、调用了什么模型、成功率多少全无记录。出了问题你连排查方向都没有。所以Token必须作为运行时凭据Runtime Credential在容器启动时动态注入且与应用代码完全解耦。3.2 推荐方案Docker Secrets 百炼Token自动刷新服务双保险我们采用“双保险”架构一层是Docker原生的Secret机制保障静态Token的安全注入另一层是自研的Token Refresh Service保障Token的长期有效性。第一步使用Docker Secrets注入初始TokenDocker Secrets是Swarm模式下的原生安全机制即使你用Docker Compose只要docker-compose.yml里声明secrets它就会在容器内挂载一个只读的/run/secrets/目录里面是加密的Token文件。# docker-compose.yml 片段 version: 3.8 services: openclaw: image: my-openclaw:latest secrets: - bai_lian_token # ... 其他配置 secrets: bai_lian_token: file: ./secrets/bai_lian_token.txt./secrets/bai_lian_token.txt是一个纯文本文件内容就是你的百炼API Key。这个文件绝不能加入Git必须加入.gitignore。启动时OpenClaw容器内会自动出现/run/secrets/bai_lian_token文件内容就是你的Token。第二步编写Token Refresh Service实现自动续期百炼Token有7天有效期。靠人工续期不现实。我们写了一个极简的Python服务token-refresher.py它的工作流程是启动时读取/run/secrets/bai_lian_token获取初始Token调用百炼的/v1/token/refresh接口需提前在百炼控制台开通Refresh Token权限将新Token写入一个共享Volume如/shared/token.jsonOpenClaw的Skill代码在每次调用百炼API前先读取/shared/token.json如果发现Token过期通过exp字段判断则自动刷新。token-refresher.py核心逻辑简化版import json import time import requests from datetime import datetime, timedelta SHARED_VOLUME /shared TOKEN_FILE f{SHARED_VOLUME}/token.json def get_fresh_token(): # 从Secret读取初始Token with open(/run/secrets/bai_lian_token, r) as f: initial_token f.read().strip() # 调用百炼刷新接口 refresh_url https://dashscope.aliyuncs.com/api/v1/token/refresh headers {Authorization: fBearer {initial_token}} response requests.post(refresh_url, headersheaders) if response.status_code 200: data response.json() new_token data[result][token] expires_in data[result][expires_in] # 秒数 # 写入共享Volume供OpenClaw读取 token_data { token: new_token, exp: int(time.time()) expires_in - 300, # 提前5分钟过期留刷新缓冲 updated_at: datetime.now().isoformat() } with open(TOKEN_FILE, w) as f: json.dump(token_data, f) print(f[INFO] Token refreshed. Expires at {datetime.fromtimestamp(token_data[exp])}) else: print(f[ERROR] Refresh failed: {response.status_code} {response.text}) if __name__ __main__: # 启动时立即刷新一次 get_fresh_token() # 每4小时刷新一次7天有效期4小时足够 while True: time.sleep(4 * 3600) get_fresh_token()docker-compose.yml中增加这个服务services: # ... openclaw service token-refresher: image: python:3.11-slim-bookworm volumes: - ./secrets:/run/secrets:ro - ./shared:/shared command: [python, /app/token-refresher.py] working_dir: /app restart: unless-stopped depends_on: - openclaw第三步OpenClaw Skill中安全调用Token在你的Skill代码里比如skills/qwen_chat.py不要直接读环境变量而是这样import json import time import requests def get_bai_lian_token(): 安全获取百炼Token自动处理刷新 try: with open(/shared/token.json, r) as f: token_data json.load(f) # 检查是否过期 if time.time() token_data.get(exp, 0): raise ValueError(Token expired) return token_data[token] except (FileNotFoundError, json.JSONDecodeError, ValueError) as e: # Token文件不存在或已过期返回一个空字符串让上层处理 print(f[WARN] Failed to get valid token: {e}) return def qwen_chat(prompt: str): token get_bai_lian_token() if not token: return {error: No valid BaiLian token available} url https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation headers { Authorization: fBearer {token}, Content-Type: application/json } payload { model: qwen-max, input: {messages: [{role: user, content: prompt}]}, parameters: {temperature: 0.8} } response requests.post(url, headersheaders, jsonpayload, timeout60) return response.json()提示这个方案的关键在于“解耦”。Token的获取、刷新、存储全部由token-refresher服务负责。OpenClaw的Skill代码只负责“消费”Token逻辑清晰职责单一。未来如果百炼换成其他大模型平台你只需要改token-refresher.py所有Skill代码一行都不用动。3.3 审计与监控让每一次Token调用都可追溯光有安全注入和自动刷新还不够。你必须知道Token被谁、在什么时候、调用了什么。我们在token-refresher服务里加了简单的日志# 在get_fresh_token()函数末尾添加 with open(f{SHARED_VOLUME}/token_audit.log, a) as log_f: log_f.write(f{datetime.now().isoformat()} | REFRESHED | {new_token[:10]}... | Expires: {datetime.fromtimestamp(token_data[exp])}\n)同时在OpenClaw的Skill调用百炼API前也加日志# 在qwen_chat()函数开头添加 print(f[AUDIT] Calling Qwen with token {token[:10]}... at {datetime.now().isoformat()})这些日志统一收集到/shared/logs/目录下再通过docker-compose logs -f token-refresher或docker-compose logs -f openclaw实时查看。一次异常调用你能立刻定位到是哪个Skill、哪个用户、哪个时间点触发的。这才是真正的“Plan”而不是“填空”。4. 技能Skill工程化从零散脚本到可维护、可测试、可灰度的生产级模块OpenClaw的核心魅力在于Skill但它的最大陷阱也在于Skill。很多人把Skill当成shell脚本的Python翻版一个.py文件几行os.system()再加个skill装饰器就完事了。结果呢一个Skill出错整个OpenClaw服务挂掉一个Skill内存泄漏服务OOM重启一个Skill调用外部API失败没有重试、没有降级、没有熔断——这就是典型的“脚本思维”不是“工程思维”。真正的Skill必须是可独立部署、可单元测试、可灰度发布、可监控告警的微服务模块。它和OpenClaw主服务的关系应该是“松耦合、强契约”而不是“紧耦合、弱约定”。4.1 Skill目录结构规范为什么必须分层、分域、分环境我见过最混乱的Skill目录是这样的skills/ ├── chat.py ├── pdf2md.py ├── video_compress.py ├── utils.py └── config.py所有东西揉在一起utils.py里混着数据库连接、日志配置、HTTP客户端config.py里硬编码着百炼Token。这种结构改一个功能牵一发而动全身。我们强制推行以下结构以pdf2md为例skills/ └── pdf2md/ ├── __init__.py # 定义Skill入口暴露skill对象 ├── main.py # 核心业务逻辑只做一件事PDF转Markdown ├── models.py # Pydantic模型定义输入输出Schema ├── services/ # 依赖服务如PDF解析服务、Markdown生成服务 │ ├── pdf_parser.py │ └── md_generator.py ├── tests/ # 单元测试覆盖核心路径 │ ├── test_main.py │ └── test_services.py └── config/ # 配置按环境分离 ├── base.py ├── dev.py └── prod.py为什么这样设计__init__.py这是OpenClaw扫描Skill的唯一入口。它只做一件事实例化一个Skill对象并用skill装饰。所有其他逻辑全部隔离在外。这样OpenClaw主进程只加载这个轻量级入口即使main.py里有bug也不会影响主进程启动。main.py遵循“单一职责原则”。它只接收输入models.Input、调用services、返回输出models.Output。没有IO、没有配置、没有日志——这些都交给services和config去管。services/把所有外部依赖PDF解析库、网络请求、文件IO封装成Service。好处是可以轻松Mock进行单元测试可以独立升级PDF解析引擎而不影响业务逻辑可以为不同PDF来源本地文件、S3 URL、Base64提供不同的Service实现。tests/每个Skill必须有测试。我们要求覆盖率不低于70%。测试用例必须覆盖正常流程、空输入、非法输入、外部服务超时、外部服务返回错误。pytestpytest-mock是标配。4.2 单元测试实战如何为一个PDF转Markdown Skill写有效测试以skills/pdf2md/main.py为例核心函数是convert_pdf_to_markdown(pdf_path: str) - str。无效测试常见错误# 错这测试的是PDF解析库不是你的Skill逻辑 def test_convert_pdf_to_markdown(): result convert_pdf_to_markdown(test.pdf) # 读取真实文件 assert 第一章 in result这个测试依赖真实文件、真实PDF库、真实网络如果PDF库需要下载字体运行慢、不稳定、无法隔离。有效测试推荐做法# tests/test_main.py import pytest from unittest.mock import patch, MagicMock from skills.pdf2md.main import convert_pdf_to_markdown from skills.pdf2md.services.pdf_parser import PDFParser from skills.pdf2md.services.md_generator import MDGenerator class TestConvertPDFToMarkdown: patch(skills.pdf2md.services.pdf_parser.PDFParser.parse) patch(skills.pdf2md.services.md_generator.MDGenerator.generate) def test_normal_flow(self, mock_generate, mock_parse): # Mock外部依赖 mock_parse.return_value [Page 1 text, Page 2 text] mock_generate.return_value # Page 1\nPage 1 text\n\n# Page 2\nPage 2 text # 调用被测函数 result convert_pdf_to_markdown(fake_path.pdf) # 断言 assert result # Page 1\nPage 1 text\n\n# Page 2\nPage 2 text mock_parse.assert_called_once_with(fake_path.pdf) mock_generate.assert_called_once_with([Page 1 text, Page 2 text]) def test_empty_input(self): with pytest.raises(ValueError, matchpdf_path cannot be empty): convert_pdf_to_markdown()这个测试的优点快毫秒级完成不碰磁盘、不碰网络稳结果100%可预测不受PDF文件内容、网络状况影响准只验证你的convert_pdf_to_markdown函数逻辑不验证PDFParser或MDGenerator的实现全覆盖了正常流程和边界条件空输入。提示pytest的patch装饰器是Skill测试的灵魂。它让你能精准控制每一个外部依赖的返回值从而穷举所有可能的执行路径。一个Skill没有单元测试就不算完成。4.3 灰度发布与A/B测试如何让新Skill上线零风险生产环境永远不要“全量上线”。我们用Docker Compose的deploy配置实现Skill的灰度发布。假设我们开发了一个新的pdf2md_v2Skill想先让10%的流量走它90%走老的pdf2md_v1。第一步修改docker-compose.yml为OpenClaw服务添加标签services: openclaw: # ... 其他配置 deploy: labels: - traefik.http.routers.openclaw.rulePathPrefix(/api/skill) - traefik.http.services.openclaw.loadbalancer.sticky.cookietrue environment: - SKILL_VERSIONv1 # 默认版本第二步在OpenClaw的Skill路由逻辑里openclaw/routing.py根据请求Header或Query Param决定用哪个Skillfrom fastapi import Request, HTTPException from skills.pdf2md_v1.main import convert_pdf_to_markdown as v1_convert from skills.pdf2md_v2.main import convert_pdf_to_markdown as v2_convert async def route_pdf2md(request: Request, pdf_path: str): # 读取请求Header如 X-Skill-Version version request.headers.get(X-Skill-Version, v1) if version v2: return await v2_convert(pdf_path) else: return await v1_convert(pdf_path)第三步用curl进行灰度测试# 100%走v1 curl -H X-Skill-Version: v1 http://localhost:8000/api/skill/pdf2md?pathtest.pdf # 100%走v2 curl -H X-Skill-Version: v2 http://localhost:8000/api/skill/pdf2md?pathtest.pdf # 随机50%走v2在Nginx或Traefik里配置 curl http://localhost:8000/api/skill/pdf2md?pathtest.pdf这样新Skill上线前你可以先在内部用X-Skill-Version: v2测试再让1%的用户通过前端埋点控制走v2监控v2的错误率、耗时、资源占用达标后再扩到10%、50%一旦发现问题X-Skill-Version: v1一键回滚。这才是现代软件工程该有的发布节奏而不是“老板说上线我就docker-compose up -d”。5. 故障排查全景图从openclaw : 无法将“openclaw”项识别为 cmdlet到生产级可观测性的完整链路部署完成服务跑起来了API也能调通。但别高兴太早。真正的考验永远在上线之后。你收到第一条告警“openclaw : 无法将“openclaw”项识别为 cmdlet、函数、脚本文件或可运行程序的名称”。这行报错99%的教程会告诉你“重启终端”“重装PowerShell”但作为生产环境的守护者你必须知道这行报错背后可能藏着从Windows PowerShell、到Docker Desktop、再到OpenClaw容器内Python环境的整整七层故障链路。我们把它拆解成一张“故障排查全景图”覆盖从用户请求发出到最终错误返回的每一个环节。5.1 第一层客户端环境Windows PowerShell / CMD这是最表层也是最容易被忽略的。openclaw : 无法将“openclaw”项识别为 cmdlet...这个报错只会在Windows的PowerShell或CMD中出现因为Linux/macOS的Shell没有cmdlet这个概念。根因分析用户在Windows上试图用openclaw命令行工具CLI来调用本地部署的OpenClaw服务但用户并没有全局安装openclawCLI或者安装了但openclaw可执行文件不在系统的PATH环境变量里PowerShell找不到openclaw.exe就抛出这个经典的“无法识别cmdlet”错误。排查步骤在PowerShell中运行Get-Command openclaw看是否返回任何信息运行$env:PATH检查Python Scripts目录如C:\Users\XXX\AppData\Roaming\Python\Python311\Scripts是否在PATH里运行pip show openclaw确认CLI包是否真的安装了。解决方案不要用Windows CLI调用生产环境的OpenClaw。CLI是开发调试工具不是生产客户端。生产环境一律用curl、Postman或前端应用调用HTTP API如果非要CLI用pipx install openclawpipx会把CLI安装到独立环境并自动加入PATH或者直接用python -m openclaw.cli serve绕过PATH查找。注意这个错误和你的云容器部署完全无关。它只发生在客户端。很多新手一看到这个报错就怀疑是Docker没跑起来开始疯狂docker ps、docker logs浪费大量时间。记住客户端报错先查客户端服务端报错再查服务端。5.2 第二层Docker容器健康状态假设客户端没问题你用curl http://localhost:8000/health返回503 Service Unavailable。这就进入了服务端排查。标准排查链路必须按顺序执行步骤命令预期输出异常含义1. 容器是否在运行docker ps | grep openclaw显示openclaw_openclaw_1一行容器已崩溃看docker logs2. 容器内进程是否存活docker exec openclaw_openclaw_1 ps aux | grep gunicorn显示gunicorn: master和worker进程主进程挂了看/app/logs/日志3. 端口是否监听docker exec openclaw_openclaw_1 netstat -tuln | grep :8000显示LISTEN状态gunicorn没启动或绑定失败4. 健康检查是否通过docker exec openclaw_openclaw_1 curl -f http://localhost:8000/health返回{status:ok}/health端点逻辑有Bug关键技巧docker logs -f openclaw_openclaw_1 --tail 100实时看最后100行日志-f表示follow像tail -f一样docker exec -it openclaw_openclaw_1 sh进入容器像登录服务器一样调试docker stats openclaw_openclaw_1看CPU、内存、网络实时占用判断是否OOM。我曾经遇到一个案例docker ps显示容器在运行ps aux也看到gunicorn