OpenClaw v2026.3.12 这版发布之后,我们项目组接到一个有点折腾的任务:把一套 AI 代理服务部署到客户的隔离网络里。那台机器没有任何外网权限,既没法直接docker pull,也没法在线执行npm install,连操作系统的软件源都得走内网镜像。整个流程硬啃下来,从源码构建到 Docker 部署踩了不少坑,也沉淀出一套能直接复用的离线交付方案。这篇教程就是这次实战的完整记录,适合要给 OpenClaw 做离线部署、或者正在 Windows 上折腾 Docker Desktop/WSL 的老哥参考,也适合那些刚接触 Docker 部署,想搞清楚"离线环境下到底怎么把东西跑起来"的朋友。
1. 为什么这次选择源码构建而不是直接拉镜像
1.1 离线环境的硬约束和唯一出路
很多人在公网机房部署服务习惯了,觉得"部署 OpenClaw?直接docker run拉个镜像不就行了"。但一旦切到隔离内网,第一条命令就会给你当头一棒:docker pull卡在连接超时,拉镜像的隧道根本建立不起来。更麻烦的是客户的安全策略,不允许搭建任何出网通道,等于把这扇门彻底焊死了。
这种情况下唯一可行的路线就是"搬运":在一台能联网的构建机上,把 OpenClaw v2026.3.12 的源码拿下来、编译好,再打成 Docker 镜像,用离线介质(U盘、内网共享盘、堡垒机传输通道)搬到目标机器上导入。之后所有依赖都必须以本地文件的形式存在,构建机就是整条链路的源头,物资是否齐全直接决定后面能不能跑通。
1.2 源码构建相比镜像拉取的三个实际收益
docker pull虽然省事,但在离线场景下反而有隐患。基础镜像的 tag 是浮动的,比如FROM node:20,今天拉是一个版本,半年后拉可能就变了。一旦构建机上的镜像缓存过期,离线环境里你根本不知道原来的镜像是不是同一个。源码构建+离线归档,等于把版本锁死在自己的 tar 包里,交付的是确定性的东西。
第二个收益是可定制性。客户环境里经常需要挂内部证书、改默认端口、加自定义 skill 文件。这些改镜像内部文件再 commit,不如在源码构建阶段就把定制项塞进去,后面维护起来至少知道改了什么。第三个收益是可审计。OpenClaw 本身是开源项目,安全团队要求提供代码来源清单和依赖清单,源码构建能顺带交付一份完整的依赖树,这个在政企项目里是硬性要求。
当然代价也有:构建机要装完整工具链,编译一次要花几十分钟,而且物资清单漏一项就得从头再来。所以后面那张清单表格特别重要,照着理一遍能省掉不少返工。
1.3 为什么锁定 v2026.3.12 这个版本号
OpenClaw 的版本号是日期制的,v2026.3.12 就是 2026 年 3 月 12 日发布的快照版本。日期版本号有个特点:每个版本对应唯一的代码状态,不会出现"v1.2.3 和 v1.2.4 修了什么要翻 changelog"的混乱感。这个版本修复了此前若干 API 兼容性问题,同时与多数主流大模型接入方式保持稳定。离线部署场景里我强烈建议锁定某个具体版本,不要用main分支或latesttag,因为你没法在离线环境里随时拉更新,一旦跑出问题连比对代码状态都做不到。
2. 构建机上的离线物资清单:工具链和依赖包一次备齐
2.1 构建机的软件基线
说句实在话,离线部署项目百分之七十的失败原因不是操作步骤错了,而是"带过去的工具不齐"。比如到目标机器上一看,没有docker load对应的 Docker 版本,或者构建机上的 Node 版本跟项目要求不匹配,编译出来的产物行为异常。先整理一张软件基线表,照着准备:
| 软件 | 版本要求 | 用途 |
|---|---|---|
| 构建机系统 | Ubuntu 22.04 LTS 或 Windows 11 | 拉取源码、执行编译 |
| Node.js | 20.x LTS | OpenClaw 运行时核心 |
| npm | 10.x(随 Node 附带) | 依赖安装与构建脚本 |
| Docker | 24.0 以上 | 镜像构建与导出 |
| Git | 2.39 以上 | 拉取指定 tag 源码 |
| tar 工具 | 系统自带 | 源码与产物打包 |
有人会问:为什么非要用 Node 20?是因为 OpenClaw 的构建脚本用到了较新的 JavaScript API,Node 18 在某些模块上会报语法错误,Node 22 又太激进,跟部分原生模块的编译链不完全兼容。直接上 20.x LTS 是最稳的选择。Docker 版本影响的是docker save出的镜像格式,24.0 以上对 OCI 格式的支持最完整。
2.2 源码与依赖包的收集策略
代码获取要分三层讲。第一层是 OpenClaw 主仓库,在构建机上执行git clone -b v2026.3.12 --depth 1,只拉这一个 tag 的历史,尽量浅克隆,省时间也省磁盘。拉完打个 tar 包,作为离线源码分发的第一份物资。
第二层是 npm 依赖。这一步是离线部署最关键的环节,因为 OpenClaw 本身有大量 npm 包依赖,在线环境下npm install一条命令搞定,离线环境必须先把所有依赖包以文件形式准备好。最稳妥的做法是在联网构建机上先跑一次完整的npm ci,安装完成之后,整个node_modules连同package-lock.json一起打压缩包。到了离线环境直接解压放到项目根目录,不需要再跑网络安装。
第三层是二进制原生模块。比如编译依赖中可能涉及的node-gyp相关包,以及某些需要预编译二进制的依赖。这类包在不同操作系统上文件不同,务必针对目标机器的系统架构单独准备。我们的目标是 Ubuntu 22.04 x86_64,跟构建机一致,所以直接把构建机上的产物搬过去没问题。如果目标机器是 ARM 架构,那构建机也必须换成 ARM,否则没法交叉编译出可用的二进制。
2.3 容易被忽略的版本锁定文件
很多人觉得"源码 Copy 过去了就完事",其实版本锁定文件才是离线构建能复现的前提。package-lock.json锁住了每个 npm 包的唯一版本,requirements.txt(如果 OpenClaw 有 Python 侧组件)锁住 Python 依赖,Dockerfile 里的FROM镜像也不能只用 tag,最好直接固定到具体的 digest 值,比如node:20@sha256:xxxx。这样哪怕构建机本身的镜像缓存被换掉,重新构建也能拿到一样的镜像内容。
还有一类容易被忽略的物资是系统级的运行库。比如目标机器在 import 某些原生模块时需要libstdc++.so.6等系统库,在线环境缺了可以apt-get install,离线就只能靠提前准备的 deb 包。用apt-get download在构建机上把依赖的 deb 包全部拉下来,连同安装脚本一起打包。这类细节前期不确认,到了目标环境就是"运行容器时报缺库,但你真的什么都做不了"。
3. 核心环节:OpenClaw 离线编译的操作链路
3.1 编译前的环境变量与 registry 开关
拿到离线物资包并解压之后,第一步不是急着编译,而是先确认构建环境完全隔离于外网。为什么要确认?因为 npm 在安装依赖时会尝试访问默认的 registry,如果当前网络策略允许出网,它会悄悄去拉包,然后你交付的产物就包含了在线环境特有的行为,到离线环境可能对不上。为了严谨,把 registry 强制指向本地空目录或内网私有源,并关闭 npm 的联网检查:
npm config set registry http://127.0.0.1:4873 npm config set fetch-retries 0 npm config set fetch-retry-mimsec 1 npm config set offline true这里127.0.0.1:4873只是占位,真正目的是让 npm 不要访问任何外网源。如果你在构建机上临时搭了本地 Verdaccio 私有 npm 缓存,可以指到这里;如果没有,设置为一个不可达的地址也能达到"禁止联网安装"的效果。
3.2 从解压到构建产物的完整过程
假设离线物资包已经落在构建机/build目录下,按下面顺序操作:
cd /build tar xzf openclaw-src-2026.3.12.tar.gz cd openclaw tar xzf node_modules_linux_x64.tar.gz # 解压预装好的依赖 cp package-lock.json package-lock.json.bak npm ci --offline --ignore-scripts=false这里有个细节:先解压预装好的node_modules,再执行npm ci --offline,目的是让 npm 校验并补齐缺失的包,而不是重装全部依赖,速度能快很多。--ignore-scripts=false很重要,因为部分原生模块的安装脚本会在node_modules解压之后自动编译二进制,如果关了脚本,后面运行时会报模块加载错误。
依赖装好之后执行正式构建:
npm run build构建过程会输出大量日志,核心关注几个关键点:TypeScript 编译是否全部通过、产物目录是否生成、是否有ERR!级别的异常。OpenClaw 的构建产物一般在dist目录下,包含编译后的 JavaScript 入口文件和静态资源。
3.3 构建产物校验的土办法
构建完成不等于构建成功,我习惯用三个土办法快速校验:
- 看
dist目录文件数量和总大小,如果数量太少或者体积异常,大概率中间某步失败了。 - 直接跑一次
node dist/index.js --version,能正常输出版本号说明运行链路基本打通。 - 检查
dist目录里有没有残留的.map源码映射文件,这类文件虽然不致命但会暴露源码路径,交付给客户前建议统一清理。
如果这三个检查都过了,源码构建这一环才算真正落地。产物不急着打包,后面还有一道 Docker 镜像的工序。
4. Docker 镜像制作与离线分发的两种姿势
4.1 Dockerfile 编写的分层思路
OpenClaw 构建机上的镜像不能直接拿来用,因为构建环境里有一大堆源码、编译缓存和多余依赖,全塞进运行镜像是安全审计的大忌。正确做法是采用多阶段构建:第一阶段负责编译,第二阶段只拷贝产物。
FROM node:20-slim AS build WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --offline COPY src ./src RUN npm run build FROM node:20-slim AS runtime WORKDIR /app ENV NODE_ENV=production COPY --from=build /app/dist ./dist COPY package.json ./ RUN npm prune --omit=dev --offline EXPOSE 8080 CMD ["node", "dist/index.js"]多阶段构建的妙处在于,最终镜像只包含运行必需的代码和依赖,源代码、编译工具、缓存文件都被隔离在中间层里。镜像体积能少一半以上,而且传给客户的安全扫描结果会好看很多。
4.2 构建细节与 .dockerignore 的作用
在构建机上执行docker build之前,先写好.dockerignore,把node_modules、dist、*.tar.gz全部排除。因为刚才我们已经在构建机上解压了依赖,如果不排除node_modules,Docker 会把整个依赖目录打进 build context,网络传输和目录扫描都会慢得离谱。
构建命令:
docker build -t openclaw:2026.3.12 -f Dockerfile .构建结束后用docker images确认镜像 ID。这里建议把镜像 tag 里带上日期版本,不要只写latest。latest在公网环境下还能忍,在离线环境里根本不知道它对应哪个版本,排查问题时完全没有参照物。
4.3 离线分发:tar 打包和私有仓库两种打法
离线分发最常见的方案是docker save导出单个 tar 文件:
docker save openclaw:2026.3.12 -o openclaw-2026.3.12.tar镜像体积一般几百 MB,如果超过 2GB 还要过移动介质,建议用split分卷再合并,split -b 1G openclaw-2026.3.12.tar openclaw.tar.part,目标机器上用cat合并回来再docker load。不过这种方案有个问题:镜像多的时候,搬运和加载都很累。如果内网里已经有一台私有仓库(比如 Harbor),更好的姿势是docker save到 tar 再推上去,或者直接在构建机docker push到内网 registry,目标机器上docker pull。注意这里说的"推"是走内网,不碰公网,完全合规。
镜像传到目标机器后的导入命令:
docker load -i openclaw-2026.3.12.tar导入完成后docker images里应该能看到openclaw:2026.3.12。如果 tag 信息没带过来,docker tag手动补一下即可。
5. 目标机器的 Docker 环境准备:Windows 是最容易翻车的地方
5.1 Linux 服务器的 Docker 离线安装
目标机器如果是一台干净的 Ubuntu 服务器,装 Docker 的方式有两种。第一种是离线 deb 包安装:在构建机上下载docker-ce、containerd、docker-ce-cli等 deb 包,拷到目标机器执行dpkg -i。第二种是内网 apt repo:在构建机搭一个简单 apt 镜像,目标机器改一下源指向内网,然后apt-get install docker-ce。
优先建议第一种。离线环境里 apt repo 的维护成本高,依赖关系容易缺,deb 包反而傻瓜式可依赖。装完先验证:
sudo systemctl enable --now docker sudo docker run --rm hello-world如果hello-world镜像没有打包在离线包里,那就直接验证docker info和服务状态,别非得跑一个陌生镜像。另外要注意 Docker 权限问题,目标机器上的普通用户如果想免 sudo 执行 docker,需要把用户加入 docker 组:sudo usermod -aG docker $USER,然后重新登录。这个坑在离线现场经常遇到,操作了半天发现是权限不对。
5.2 Windows 桌面机的 Docker Desktop 与 WSL2 问题集
如果你的目标机器是 Windows,那 Docker Desktop 的安装可能比想象中麻烦,这里集中说一下热词里高频出现的几个问题。
第一个是"Docker Desktop failed to start because virtualisation support wasn't detected"。这个报错翻译成人话就是:Docker Desktop 需要虚拟机监控程序,但是你的系统没开。排查顺序是:重启进 BIOS/UEFI,把 Intel VT-x(或 AMD SVM)打开;然后在 Windows 功能里启用"虚拟机平台"和"Hyper-V"(Windows 11 家庭版可能没有 Hyper-V 选项,但"虚拟机平台"必须开);最后确认 Windows 沙盒或 WSL2 相关功能没有被组策略禁用。
第二个是"openclaw 无法安全验证"以及 PowerShell 中运行wsl --status的问题。Windows 下从网上下载的脚本、压缩包往往会被打上 Mark of the Web 标记,双击执行时系统提示"无法安全验证此文件"。解决办法是在 PowerShell 里对相关文件执行:
Unblock-File -Path .\openclaw-windows-companion.ps1如果是 WSL 状态异常,运行wsl --status或wsl --version查看具体是哪个组件没就绪。常见的坑是wsl --install之后没有重启,或者 WSL 内核太旧,去微软官方更新一下 WSL2 内核包就能解决。
第三个问题是 Docker Desktop 启动一直转圈,最后报virtualisation support wasn't detected的变体。这里除了 BIOS 开关之外,还要确认 Windows 的虚拟化安全(VBS)没有和 Docker Desktop 打架。可以先在 PowerShell 里跑systeminfo,看末尾有没有"Hyper-V 要求: 已检测到虚拟机监控程序。将不启用 Hyper-V"这类提示,如果出现说明有别的虚拟机监控程序占着,用bcdedit /set hypervisorlaunchtype auto恢复默认。
5.3 GPU 透传:NVIDIA Container Toolkit
如果 OpenClaw 后面要接本地推理模型,目标机器有 NVIDIA GPU 的话,容器里想直接用 GPU 必须要装 NVIDIA Container Toolkit。Ubuntu 上离线安装时,同样要先把nvidia-container-toolkit的 deb 包和 CUDA 驱动 deb 包准备好,然后:
sudo apt-get install -y ./nvidia-container-toolkit*.deb sudo nvidia-ctk runtime configure --runtime=docker sudo systemctl restart docker装完后在容器里执行nvidia-smi能看到显卡信息就说明透传成功。注意 Docker 的 GPU 能力在离线环境不会自动开启,必须在docker run里加--gpus all,或者在 compose 文件里声明gpu资源。
6. 容器化部署 OpenClaw 的编排与关键配置
6.1 docker-compose 编排服务
OpenClaw 生产部署不太建议docker run一条命令一把梭,参数太多容易漏。用docker-compose.yml把网络、端口、数据卷、环境变量收敛到一份文件里,后期维护也方便。
version: "3.8" services: openclaw: image: openclaw:2026.3.12 container_name: openclaw restart: unless-stopped ports: - "8080:8080" volumes: - ./data:/app/data - ./skills:/app/skills environment: - OPENCLAW_LOG_LEVEL=info - MODEL_PROVIDER=${MODEL_PROVIDER} - API_KEY=${API_KEY} - COMPANION_PORT=8901 extra_hosts: - "host.docker.internal:host-gateway"这里skills目录挂载到外部是很有用的设计,OpenClaw 的技能插件要频繁增改,如果写死在镜像里,改一次就要重新 build 一次。挂载出来后,直接编辑宿主机的 YAML 或脚本就能热更新 skill 定义。extra_hosts那行是为了让容器里访问宿主机的本地模型服务更方便,后面会用到。
6.2 大模型算力接入:外部 API、本地模型服务两种方式
热词里有人问"OpenClaw 只能用接入 API 的方式使用算力吗",答案是:不一定。OpenClaw 的模型层设计上支持两种路径,一种是通过 HTTP 协议接入外部大模型 API,另一种是把本地推理引擎(比如 Ollama、vLLM、SGLang)暴露的服务接到 OpenClaw 上。在离线环境里,如果客户不允许调用公有云 API,就必须走本地路径。
本地路径的常见做法是:在宿主机或另一台内网 GPU 机器上部署一个 Ollama 或 SGLang 服务,比如给 Qwen2.5-3B 或 Qwen3 系列离线跑起来,然后 OpenClaw 里的MODEL_PROVIDER配成openai兼容模式,API_KEY填一个任意占位值,base_url指向宿主机地址。compose 文件里写了host.docker.internal就是干这个用的,模型服务的地址可以配成http://host.docker.internal:11434/v1。
如果用 SGLang 做离线部署,命令大概是:
python -m sglang.launch_server --model-path /models/Qwen3-8B --host 0.0.0.0 --port 30000然后把 OpenClaw 的模型 endpoint 指到这个地址。但要注意容器和服务之间的网络连通性:如果推理服务跑在宿主机,端口必须监听0.0.0.0而不是127.0.0.1,否则容器外面访问不到,这个坑我踩过一次。
6.3 skill 与 Windows Companion 的配置
OpenClaw 的 skill 机制在部署层面没什么特别的,就是约定目录结构。在./skills下按规范放好技能定义文件,容器启动后扫描该目录。新增 skill 不需要重启容器,只要确保挂载目录权限正确,容器内用户能读就行。权限问题在 Linux 下容易碰到:宿主机目录属主是 1000,容器内进程是 root,可能读不了,最简单是chmod -R 755 skills。
Windows Companion 的热词说明不少用户想把这套东西接到 Windows 桌面端。Companion 本质上是一个消息通知和任务下发通道,OpenClaw 容器需要暴露一个额外端口(上面 compose 里写了8901),Companion 客户端通过这个端口跟代理服务通信。配置时把地址填成运行 OpenClaw 的机器 IP,端口对应 8901,再把安全令牌配置一致即可。离线环境里没有公网推送服务,全靠局域网内网直连,这个设计反而更安全。
7. 部署后的验证链路与高频故障排查
7.1 三步验证:日志、健康检查、端到端对话
部署完不能直接拍屁股走人,按下面的顺序做验证:
第一步看日志。docker logs -f openclaw,确认启动过程没有UnhandledPromiseRejection或模块加载失败。OpenClaw 正常启动会打印当前版本号和监听端口。
第二步打健康检查。如果是 HTTP 服务,直接curl http://localhost:8080/health或/api/v1/ping,看返回是否正常。要在宿主机上测,确认端口映射已经生效。
第三步是端到端测试。给 OpenClaw 发一条最简单的指令,让它调用配置好的模型服务生成一个回复体。这一条能同时验证模型接入、skill 加载和消息链路三个环节。如果在本地模型路径下失败了,优先看推理服务日志和 OpenClaw 传过去的base_url是否拼对。
7.2 高频问题速查表
我把这次离线部署中实际遇到过的问题整理成表,方便各位遇到同款时直接对号入座:
| 故障现象 | 根因 | 处理方式 |
|---|---|---|
| Docker Desktop 启动报虚拟化检测失败 | BIOS 未开 VT-x/AMD-V | 进 BIOS 开启,并启用 Windows 虚拟机平台 |
wsl --status显示状态异常 | WSL2 内核未升级或未重启 | 更新 WSL2 内核包,重开终端后再wsl --shutdown |
| OpenClaw 脚本提示无法安全验证 | Windows Mark of the Web 拦截 | Unblock-File解除标记 |
| 容器内访问宿主机模型服务超时 | 服务只监听了 127.0.0.1 | 改成 0.0.0.0,compose 里加host.docker.internal |
镜像导入后 tag 变成<none> | docker load未带 meta 信息 | docker tag手动补 tag |
| 容器网络不通 | 自定义 bridge 网络冲突 | 换默认 bridge 或清理自定义网络 |
| 容器内 npm 模块加载报错 | node_modules与当前平台不匹配 | 重新用目标平台构建依赖 |
| Docker 权限错误 | 当前用户不在 docker 组 | usermod -aG docker $USER后重新登录 |
| 挂载的 skills 目录无法读取 | 目录权限不足 | chmod -R 755 skills |
| 多个容器端口映射冲突 | 端口被占用 | netstat -tulpn查占用后换端口 |
7.3 离线交付的几条硬经验
最后分享几条这次实操攒下来的经验,算是不写进文档的软知识。
第一,整个离线物资包在打包前做一次完整校验。源码 tar 包、node_modules 压缩包、镜像 tar、deb 包,每一个都算好 SHA256 写在清单文本里,到了现场核对一遍。离线环境出问题想重传导数据很难,清单能帮你快速定位是哪一个包坏了。
第二,构建机不要把npm prune之后的依赖当交付物。npm prune会删掉 devDependencies,如果后面需要在目标机器上重新跑一次编译,没有 devDependencies 就废了。交付的依赖包应该是完整的node_modules,而不是瘦身版。
第三,镜像搬运最好带一份"镜像说明"。写上这个镜像是基于哪个提交构建的、构建时间、有没有挂载外部卷、默认端口多少。现场的人可能不是你,一份说明能省掉无数个电话。
这个项目做完之后我才真正体会到,离线部署考验的往往不是技术深度,而是筹备阶段的缜密程度。你永远不可能在目标机器上临时"变"出一个依赖来,所以每一份物资、每一个版本号、每一层镜像分层,都得在出发前确定。以后接手类似的内网项目,我都会先把这套流程走一遍——准确说,先在本地虚拟机演练一遍再打包,到了客户现场基本就是几分钟的事。