用 Docker 部署 OpenClaw 这件事,其实坑不在 Docker 本身,而在编译、迁移和 Token 配置这三个环节。最近帮朋友迁移一台跑了半年多的 OpenClaw 服务,数据卷、镜像、环境变量一路折腾下来,踩了不少雷。这篇就把完整过程写出来,给准备自己部署或者正在迁移的朋友参考——内容全部围绕实际操作展开,从架构选型讲到具体命令,最后附上我整理的排错清单,照着走能省下不少时间。
1. 部署前先想清楚:OpenClaw 为什么值得用 Docker 跑
1.1 OpenClaw 是个什么项目
OpenClaw 从名字上看就知道和机器人控制有些渊源,实际它是一个开源的智能体控制框架,可以在 ROS2 环境下跑,也能单独以服务方式运行。简单理解就是:你给它一个任务目标,它可以调用外部模型服务来拆解任务,再通过内部的 skill 体系去执行具体的操作,比如控制硬件、读写文件、调用 API 等。构建在机器人场景时,它和 ROS2 Humble、Gazebo 模拟器的配合比较常见;跑在普通服务器上时,它就是一个偏向自动化的智能体服务。
很多朋友第一次接触这个项目都是在 GitHub 上看到 README,发现官方推荐用 Docker 部署。为什么要用容器?因为 OpenClaw 的依赖链条实在不算短——Python 虚拟环境、ROS2 的底层库、Colcon 构建工具、外部的模型 SDK,散落在宿主机上很容易相互污染。Docker 把这一整套依赖关进一个隔离环境里,迁移的时候打包就走,这正好对应了标题里"编译、迁移、Token 配置"三件事,也是这篇文章的主线。
1.2 容器化部署的核心收益在哪里
我实际用下来,Docker 部署 OpenClaw 的收益主要体现在三个地方。
第一是环境一致。你在一台机器上编译好的二进制,换一台机器往往因为系统库版本对不上直接跑不起来。把编译产物连同运行时依赖锁进镜像里,这个问题就消失了。第二是迁移友好。数据卷加环境变量文件,整个服务就能从旧机器搬到新机器,不用在宿主机上留下一堆需要手工清理的临时文件。第三是权限隔离。OpenClaw 在执行任务时可能要访问宿主机目录、操作外设,容器里做一层映射,即便某个 skill 出了安全问题,也不会直接爆掉整台服务器。
当然容器化也不是没有代价:镜像构建时间长、存储占用大、网络模式复杂的时候排查麻烦。但对比收益,这点代价是值得的。如果你只用源码方式在裸机上跑过 OpenClaw,试一次 Docker 部署就会明显感觉到差距。
1.3 部署架构与目录规划
在动手之前,建议先把部署架构在脑子里过一遍。我采用的部署结构大概是这样的:
- Docker 镜像:分为两类,一类是编译镜像,包含完整构建工具链;另一类是运行镜像,只保留运行时依赖和编译产物。
- Docker 卷:至少规划两个,一个放 OpenClaw 的配置与 skill 数据,另一个放模型缓存或者日志输出。
- 环境变量:Token、服务端口、日志级别这类运行时参数全部走环境变量注入,不写死在镜像里。
- 容器编排:使用 docker compose 管理,服务名固定,方便后续更新和迁移。
目录规划上,推荐在宿主机建立一个专门的目录,比如/opt/openclaw,里面放.env、docker-compose.yml、backup/三个东西。这样不管怎么折腾,核心资产都集中在一个目录里,备份和迁移思路都非常清晰。
2. 源码编译:把 OpenClaw 变成可运行的 Docker 镜像
2.1 基础镜像怎么选
编译 OpenClaw 的第一步是选择基础镜像。根据项目特性,我推荐 Ubuntu 22.04 作为底层系统,理由很简单:ROS2 Humble 官方支持的就是 Ubuntu 22.04,OpenClaw 的构建脚本默认也是在这个版本上测试的。选非 LTS 或者太新的系统,反而容易因为依赖版本不匹配而编译失败。
如果你的场景涉及模型推理,比如要调用本地 Ollama 或者其他 GPU 推理服务,可以考虑在基础镜像上叠加 CUDA 运行时相关组件。但这里有个原则:编译阶段尽量轻量,运行阶段再按需加 GPU 支持。不要在编译镜像里塞一堆运行时才需要的东西,否则镜像体积会非常难看。
我常用的基础镜像组合是ubuntu:22.04加官方 ROS2 Humble 的 apt 源,配合 Python 3.10 的虚拟环境。如果你需要 ROS2 的完整环境,也可以直接用ros:humble作为基础镜像,省去手动安装 ROS2 的步骤,但镜像体积会大不少,按需取舍。
2.2 多阶段构建 Dockerfile 实例
多阶段构建是编译类镜像的最佳实践。它的核心思路是在第一阶段安装全部编译工具链并完成编译,第二阶段只复制编译产物和运行所需的最小依赖,这样最终镜像不包含源码、临时文件和编译缓存,安全性和体积都更优。
我提供一个简化版的 Dockerfile 作为参考:
# 阶段一:编译 FROM ubuntu:22.04 AS builder ENV DEBIAN_FRONTEND=noninteractive # 安装编译工具链和系统依赖 RUN apt-get update && apt-get install -y \ build-essential cmake git python3 python3-pip \ python3-venv colcon-common-extensions \ ros-humble-ros-base \ && rm -rf /var/lib/apt/lists/* # 创建工作目录并拷贝源码 WORKDIR /src COPY . . # 创建虚拟环境并安装 Python 依赖 RUN python3 -m venv /opt/openclaw/venv && \ . /opt/openclaw/venv/bin/activate && \ pip install --no-cache-dir -r requirements.txt # 编译 ROS2 相关组件 RUN . /opt/ros/humble/setup.sh && \ cd /src/ros_ws && \ colcon build \ --cmake-args -DCMAKE_BUILD_TYPE=Release \ --parallel-workers 4 # 阶段二:运行 FROM ubuntu:22.04 AS runtime ENV DEBIAN_FRONTEND=noninteractive # 只安装运行时依赖 RUN apt-get update && apt-get install -y \ python3 python3-venv \ ros-humble-ros-base \ curl \ && rm -rf /var/lib/apt/lists/* # 从编译阶段复制虚拟环境和编译产物 COPY --from=builder /opt/openclaw/venv /opt/openclaw/venv COPY --from=builder /src/ros_ws/install /opt/openclaw/ros_install WORKDIR /opt/openclaw # 入口脚本 COPY entrypoint.sh /opt/openclaw/entrypoint.sh RUN chmod +x /opt/openclaw/entrypoint.sh ENTRYPOINT ["/opt/openclaw/entrypoint.sh"]这个 Dockerfile 有几点值得注意。--parallel-workers 4是编译时的并行度参数,视 CPU 核数调整,我建议设为 CPU 核心数减一,避免编译时整个机器卡死。DEBIAN_FRONTEND=noninteractive必须加上,否则 apt 在容器里会等待交互输入导致构建卡住。COPY . .之前记得加.dockerignore,把.git、__pycache__、ros_ws/build、ros_ws/log等目录排除掉,否则 Docker 会把大量无用文件发到构建上下文里,不仅慢,还容易让缓存失效。
2.3 编译缓存与镜像瘦身的技巧
编译类镜像最容易犯的错误就是每次构建都全量重编。我的做法是用 Docker 的 BuildKit 缓存挂载,把编译工具自己的缓存目录映射为外部缓存,比如在 apt 安装步骤加一行:
RUN --mount=type=cache,target=/var/cache/apt \ apt-get update && apt-get install -y ...这样 apt 的 deb 包会缓存下来,下一次构建只要版本没变就秒过。
Python 依赖下载也同理,可以用pip的 cache 目录挂载:
RUN --mount=type=cache,target=/root/.cache/pip \ pip install --no-cache-dir -r requirements.txt注意这里有个细节:--no-cache-dir是让 pip 不保留临时文件,但 BuildKit 的 cache 挂载依然会把下载缓存写到挂载目录里,两者并不冲突,实测下来构建时间能缩短一半以上。
镜像瘦身方面,有一条我踩过坑的经验:不要在运行阶段把整个/opt/ros/humble目录从 builder 复制过来,那样镜像体积直接奔着 5GB 去了。ROS2 的包采用 overlay 方式,运行时只要保留install目录里的库和可执行文件,配合合适的setup.sh环境变量就能正常工作。控制住基础镜像和运行依赖之后,OpenClaw 的运行镜像应该能压在 1GB 左右,这在服务器硬盘上负担就小很多了。
3. 跨机器迁移:数据卷、镜像与配置的搬移方案
3.1 迁移前需要盘点哪些资产
"迁移"这个词很多人一听就觉得是打包整个容器,实际上 Docker 世界里的迁移思路完全不同。容器本身是临时对象,真正需要迁移的是三类资产:镜像、数据卷、环境变量配置。
- 镜像:可以重新构建,也可以从旧机器导出再导入。重新构建是最干净的,但如果在旧机器上手工改过容器内部文件,重新构建就丢掉了这些改动。所以我的原则是:一切对容器内部的修改都要通过 Dockerfile 或环境变量体现,不要直接
docker exec进去乱改。 - 数据卷:OpenClaw 的配置、skill 文件、日志、模型缓存都在卷里,这是迁移的重头戏。
- 环境变量:包括 Token、服务端口、日志级别等,通常存在
.env文件里,直接拷贝即可。
盘点完这三样,再确认新旧机器的 Docker 版本和存储驱动是否一致。曾经有一台旧机器用的是 vfs 存储驱动,导出的卷在 overlay2 的新机器上解压完毕之后目录权限全乱了,这种问题特别隐蔽。
3.2 数据卷导出导入的具体操作
数据卷迁移我推荐用最稳妥的 tar 方式,步骤很简单,先导出:
docker run --rm \ -v openclaw_data:/data \ -v $(pwd)/backup:/backup \ ubuntu tar czf /backup/openclaw_data.tar.gz -C /data .这条命令启动一个临时容器,把openclaw_data卷挂载到容器内的/data,再把当前目录下的backup文件夹挂载到/backup,最后用 tar 打包。--rm确保临时容器用完即删,不会残留。
在新机器的 Docker 上执行导入:
docker volume create openclaw_data docker run --rm \ -v openclaw_data:/data \ -v $(pwd)/backup:/backup \ ubuntu tar xzf /backup/openclaw_data.tar.gz -C /data这两条命令看起来对称,但实际操作里我见过不少人栽在卷名的坑上。比如旧机器上卷名叫openclaw_data,但在 docker-compose 里定义的服务名不同,或者项目目录名变了,docker compose 会自动在卷名前加项目前缀,导致新机器上实际挂载的卷名和预期不一样。建议在迁移之前先用docker volume ls确认一遍新旧环境里卷的准确名称,再执行导出导入。
3.3 迁移后的权限修复与环境对齐
数据卷迁移完,最常见的故障就是权限不对。容器内进程通常以非 root 用户运行,而 tar 解压后文件属主还是打包时的 UID/GID。如果旧容器里 OpenClaw 是以 UID 1000 运行的,新容器的 Dockerfile 里定义的用户是 UID 1001,那就会有权限问题。
解决办法有两种。一种是在迁移前统一 UID,把容器用户的 UID 调成一致;另一种是迁移后进入容器内执行权限修复:
docker compose exec openclaw chown -R openclaw:openclaw /data第二种方案虽然简单,但要注意先确认容器内的用户名和 UID,不要凭感觉改。还有一点容易被忽略:迁移后新旧机器如果时区不一致,日志时间戳会对不上。建议在docker-compose.yml里统一设置TZ环境变量,比如TZ=Asia/Shanghai,这样不管迁移到哪里,日志和任务计划都会按照预期时区运行。
配置对齐方面,我习惯在迁移后跑一个diff,把旧机器上的.env和新机器上的.env做一次对比,确认所有变量都拷贝完整。曾经因为漏拷一个HTTP_PROXY变量,导致新环境里 OpenClaw 的外部模型调用全部超时,排查了很久才发现。
4. Token 配置:从环境变量到密钥管理的完整方案
4.1 Token 的作用与存放位置
OpenClaw 作为一个智能体框架,运行时要和外部模型服务交互,Token 就是它的身份凭证。把这个 Token 写死在代码里是最糟糕的做法——镜像一旦构建完成,Token 会留在镜像的每一层历史里,任何人拿到镜像就能用docker history翻出来。
正确做法是把 Token 放进环境变量,在容器启动时注入。Docker 的环境变量机制本身很简单,但实际使用中有几个点需要强调。
第一,Token 不要出现在docker-compose.yml里。这个文件通常是版本管理的,如果 push 到远端仓库,Token 就泄漏了。第二,Token 不要出现在 shell 历史里。用docker run -e TOKEN=xxx这种方式,Token 会出现在进程参数里,被系统日志记录。第三,Token 要放在.env文件中,并且明确加入.gitignore。
4.2 docker compose 里注入 Token 的三种方式
我梳理一下实践中常用的三种 Token 注入方式,各有适用场景。
第一种是.env文件方式。在/opt/openclaw目录下创建.env:
OPENCLAW_API_TOKEN=sk-xxxxx OPENCLAW_PORT=8080 LOG_LEVEL=info然后docker-compose.yml 里直接引用:
services: openclaw: image: openclaw:latest env_file: - .env这种方式最直观,适合单机部署。
第二种是docker compose的变量替换方式,适合需要区分默认值和实际值的场景:
services: openclaw: image: openclaw:latest environment: OPENCLAW_API_TOKEN: ${OPENCLAW_API_TOKEN:-default-token}这里${OPENCLAW_API_TOKEN:-default-token}表示如果 shell 环境里有这个变量就用 shell 里的值,否则用default-token。这种方式便于在 CI/CD 流水线里临时覆盖配置。
第三种是 Docker Secrets,适合多容器集群和自动化平台。但单机 Docker Compose 对 Secrets 的原生支持还不够好,我用得不多。如果上了 Swarm 或者 Kubernetes,再用这种方案也不迟。
4.3 Token 失效、转义和轮换的排查
Token 配置好之后,最大的噩梦就是"明明配了怎么不生效"。我遇到过三种典型情况。
第一种是.env文件里的引号问题。.env文件解析规则比较严,如果你写成TOKEN="sk-abc",有些版本会保留引号,导致发送出去的头变成"sk-abc",服务端直接拒绝。我的建议是.env里一律不要加引号,Token 里的特殊字符也不要做额外转义,除非确实包含#或空格。
第二种是环境变量覆盖问题。如果你同时用了env_file和environment两个字段,docker compose 的规则是environment优先于env_file。调试的时候看到环境变量和预期不一致,先检查是不是这两个字段打架了。
第三种是 Token 轮换之后没重启容器。环境变量是容器创建时就确定的,修改.env文件后必须重新执行:
docker compose up -d注意是up -d,不是exec。很多朋友改了.env后只执行docker compose restart,结果容器还是旧的环境变量,白白折腾半天。验证配置是否生效,可以执行docker compose config查看最终渲染结果,也可以进容器里执行env | grep TOKEN确认真实值。
5. 实操中的高频问题与排查实录
5.1 容器启动即退出的 5 个常见原因
OpenClaw 容器启动后立刻退出,是新手遇到最多的故障,我总结下来有五个高频原因。
第一,入口脚本没有执行权限。镜像里COPY entrypoint.sh之后没有chmod +x,启动时直接报permission denied。解决方法是在 Dockerfile 里加一行RUN chmod +x /opt/openclaw/entrypoint.sh。第二,环境变量缺失导致程序直接报错退出。排查方法是用docker logs查看容器日志,通常能看到缺失变量名。第三,Token 无效导致启动时的健康检查失败,框架会主动退出等待配置修正。第四,端口被宿主机其他进程占用,报错信息里有address already in use。第五,数据卷权限异常,框架没有能力写日志文件,也会直接退出。
排查顺序我建议固定为:先docker logs看日志,再docker inspect看挂载和环境变量,最后看端口冲突。不要一上来就改配置,那样最容易把问题复杂化。
5.2 编译慢、构建缓存失效的优化手段
编译 OpenClaw 的 ROS2 组件是耗时大户,尤其是 ARM 设备上,全量编译动辄一两个小时。除了前面提到的 BuildKit 缓存挂载,还有两个优化手段值得一试。
一个是调整colcon build的并行参数。默认情况下 colcon 会尽量用满所有核心,但这会导致在内存有限的机器上 OOM。我建议用--parallel-workers 4配合--executor sequential或者按需调整,让编译过程更加可控。另一个是避免重复全量编译。colcon build --symlink-install这个参数很管用,它会让 ROS2 的 install 目录里放置符号链接而不是复制文件,源码修改后不需要重新全量编译,对迭代开发特别友好。
构建缓存失效是一个隐蔽问题。Docker 的层缓存有一个特点:只要COPY . .这一步的内容发生变化,之后所有步骤都会重新执行。解决思路是充分利用依赖分层——把requirements.txt单独先 COPY 进镜像,安装完依赖之后再 COPY 整个源码目录。这个顺序调整看起来很简单,实际效果非常显著。
5.3 一个真实的迁移翻车案例
最后分享一个我最近遇到的真实案例。新机器是 ARM 架构的服务器,旧机器是 x86 架构,我把旧机器上构建好的镜像直接导出导入到新机器,启动时报exec format error。这个错误的原因很直接:容器内可执行文件的架构和宿主机不匹配。
排查过程是这样的,先确认新旧机器 CPU 架构:uname -m,旧机器输出x86_64,新机器输出aarch64。确认就是架构问题之后,回到新机器上直接用源码重新构建镜像。因为 Dockerfile 里已经做了多阶段构建和缓存优化,重新构建的速度比预期快很多。这次之后我也长记性了:跨机器迁移之前,第一件事确认架构,第二件事确认 Docker 版本,第三件事才谈数据迁移。
我的建议是把这三步做成一个简单的检查脚本,迁移之前跑一遍,能够避免大量无意义的操作。
最后再分享一个小技巧:如果你准备长期维护 OpenClaw,建议把数据卷的备份做成定时任务,每天自动打包到宿主机的一个外部目录。备份脚本很简单,就是前面提到的那两行docker runtar 命令。数据卷是这套部署里最值钱的资产,镜像丢了可以重新构建,Token 丢了可以重新申请,但 skill 数据和配置一旦丢了,靠记忆重新恢复的成本是非常高的。定时备份加上迁移前的架构检查,这两件事做到位,OpenClaw 的 Docker 部署基本就不会出大问题。