我一直觉得,像 OpenClaw 这类带“技能系统”的 AI 代理工具,最让人头疼的不是怎么把功能跑起来,而是它口袋里的那串 API 密钥。命令行一启动,配置文件一读取,密钥就像家门钥匙压在门口地垫下面——方便是方便,可谁路过都能翻一翻。后来我索性用 Docker 把 OpenClaw 整个关进沙盒,环境隔离、密钥注入、文件权限全部重做了一遍,实测下来确实踏实多了。这篇文章就聊聊我怎么做的,以及容器化之后遇到的那些坑。
如果你也在用 OpenClaw,或者准备部署一个需要调用大模型 API 的自动化代理,这篇文章的整套思路可以直接照搬。无论你是跑在 Windows 的 Docker Desktop 上,还是 Ubuntu 服务器上用 Docker Engine,重点只有一个:让密钥不在镜像里、不在代码里、不在日志里,只在运行时的环境变量里存在。
1. 为什么我非要把 OpenClaw 塞进 Docker:密钥裸奔那点事
1.1 裸奔的密钥是怎么“漏”出去的
先说一个我自己的真实翻车现场。最早我把 OpenClaw 直接装在本地 Linux 机器上,配置文件config.yaml里理所当然写着api_key: sk-xxxx。当时想着反正是单机,不联网就行,结果有次调试某个技能,OpenClaw 的日志模块把整个配置对象打印了出来,包括完整密钥。日志文件刚好我又同步到了网盘里,等于钥匙复制了好几把放在公共储物柜里。
这还不是最离谱的。如果你装了第三方技能包,技能代码运行在和你 OpenClaw 同样的用户权限下,它完全可以读配置文件、读环境变量,把密钥悄悄传出去。OpenClaw 的技能机制很灵活,但越灵活越要防一手。API 密钥这东西,一旦泄露就是真金白银的消耗,按 token 计费的模型跑个一晚上,账单能让你怀疑人生。
另一个常见坑是 Git 仓库。很多人喜欢把配置目录直接纳入版本管理,一个git push到公开仓库,密钥就永远躺在提交历史里了。就算你马上删掉,历史里依然能翻出来。所以,密钥必须和代码、配置文件彻底分离。
1.2 沙盒隔离到底隔离了什么
Docker 在这里解决的不是“性能”问题,而是“边界”问题。它给 OpenClaw 划了一个独立的小房间,房间里的文件系统和宿主机是隔开的,进程也看不到外面的进程。更关键的是,镜像构建完是只读的,运行时你通过环境变量把密钥“注射”进去,密钥不会写进磁盘,也不会进入镜像层。
我自己理解的沙盒有三层:
- 文件隔离:容器里默认看不到宿主机上的
~/.ssh、/home/用户这些敏感目录,除非你手动挂载。 - 进程隔离:容器内的 OpenClaw 即使被恶意技能攻击,它也拿不到宿主机的 root 权限,更碰不到其他容器。
- 网络隔离:可以用
--network指定容器网络,控制它能访问哪些服务,外部想访问 OpenClaw 也得经过端口映射。
有了这三层,API 密钥就不再是“躺在文件里的明文”,而是“运行内存里的临时值”。这就是我后来坚持容器化的根本原因。
2. 环境准备:从 Docker Desktop 到 Linux 引擎的安装与避坑
2.1 Docker Desktop 还是 Docker Engine:按平台选
我日常开发主力机是 Windows,服务器是 Ubuntu。两边的选型不一样。
Windows 上推荐直接用 Docker Desktop,它自带图形界面、文件共享、WSL2 集成,对新手最友好。下载安装包,装完重启,基本就能用。不过它依赖 Windows 的虚拟化功能,如果你的机器太老或者虚拟机平台没开,大概率会启动失败。
Linux 服务器上就没必要装 Docker Desktop 了,直接用 Docker Engine 更轻量。Ubuntu 下的安装命令很简单:
sudo apt update sudo apt install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin装完验证一下docker version,能看到 client 和 server 版本就说明守护进程正常。
如果你的系统是 CentOS 或者 Fedora,命令稍有不同,但思路一样:装docker-ce和docker-compose-plugin。别再用老的docker-compose独立二进制了,新项目直接docker compose子命令更顺手。
2.2 启动 Docker 时最常见的两个拦路虎
装完 Docker 后最容易遇到两个错误,我帮朋友排查N次了。
第一个是 Windows 上 Docker Desktop 弹窗:Docker Desktop failed to start because virtualization support wasn't detected。这基本是 BIOS 里的虚拟化没开。重启进 BIOS,找Intel Virtualization Technology (VT-x)或者AMD SVM选项,启用它。注意有的主板默认是禁用,装好了 Docker 才发现这事,确实很折腾。另外如果开了 WSL2,还要保证 Windows 的“虚拟机平台”功能是启用状态,可以在管理员 PowerShell 里跑dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart。
第二个是 Linux 下报permission denied while trying to connect to the Docker daemon socket。原因是当前用户不在docker用户组里。解决办法:
sudo usermod -aG docker $USER newgrp docker然后重新登录终端,再docker ps试试。注意如果之前用的是sudo docker,之后尽量别混用,组权限和 sudo 权限管理思路不同,混用容易搞乱目录权限。
还有一个镜像拉取慢的问题。如果你在国内网络环境,直接从 Docker Hub 拉镜像确实会慢得让人抓狂。我的做法是配置一个可信的镜像加速地址,在/etc/docker/daemon.json里写好 registry-mirrors,然后重启 docker 服务。不同加速源稳定性有差异,建议至少配置两个。
3. 构建 OpenClaw 容器:官方镜像、Dockerfile、Compose 三选一
3.1 官方镜像直跑,快速验证
如果你只是想在几分钟内把 OpenClaw 跑起来看看效果,直接用官方镜像最快。假设项目官方维护了镜像(我用的是 openclaw/openclaw 这个 tag 示例,具体以你所在仓库为准):
docker run -d \ --name openclaw \ -p 8080:8080 \ -e OPENAI_API_KEY="sk-xxxx" \ -v openclaw-data:/var/lib/openclaw \ openclaw/openclaw:latest这个命令干了几件事:
-d后台运行;-p 8080:8080把容器的 Web 端口映射到宿主机;-e OPENAI_API_KEY注入密钥;-v openclaw-data创建命名卷,把持久化数据存到卷里,以后删容器也不丢数据。
跑起来之后浏览器打开http://localhost:8080,能看到 OpenClaw 的控制台,就说明基础环境通了。
但官方镜像不一定随时覆盖你的需求。比如你想加自己的技能包,或者内置某个 Python 依赖,官方镜像里可能没有。这时候就得自己写 Dockerfile,或者用 Compose 挂载技能目录。
3.2 自定义镜像:把技能和依赖打包进去
我第二次部署就用了自定义镜像,因为我在 OpenClaw 里加了几个自定义技能,需要用到requests、beautifulsoup4这类库。官方镜像的虚拟环境里没有,所以我直接基于 Python 官方镜像来构建。
FROM python:3.11-slim WORKDIR /app # 安装 OpenClaw 及其依赖 RUN pip install --no-cache-dir openclaw # 拷贝技能目录和示例配置 COPY skills/ ./skills/ COPY config.example.yaml ./config.yaml # 以非 root 用户运行,降低特权风险 RUN useradd -m -u 1000 openclaw && \ chown -R openclaw:openclaw /app USER openclaw EXPOSE 8080 CMD ["openclaw", "serve"]这里有两个细节很关键。
第一,COPY config.example.yaml ./config.yaml拷进去的是一个不含密钥的模板文件,密钥必须靠运行时环境变量注入。防止有人直接docker cp拿走你的镜像,在镜像层里翻出密钥。
第二,用非 root 用户运行。容器里默认是 root,一旦 OpenClaw 被远程命令注入攻击,攻击者可能是 root 身份。虽然容器内 root 和宿主机 root 不完全等价,但权限还是越小越好。我这里创建了 uid=1000 的用户,和宿主机普通用户对等。
构建命令:
docker build -t openclaw-custom:latest .之后运行和官方镜像类似,只是镜像是你自己打出来的。
3.3 Compose 编排:OpenClaw + Ollama + 日志服务
当你开始加周边组件时,单条docker run就不够了。我现在用 Docker Compose 管理 OpenClaw 和本地大模型服务,一个compose.yaml文件搞定:
services: openclaw: build: . image: openclaw-custom:latest env_file: - .env ports: - "8080:8080" volumes: - ./data:/app/data - ./skills:/app/skills depends_on: - ollama restart: unless-stopped ollama: image: ollama/ollama:latest ports: - "11434:11434" volumes: - ollama-models:/root/.ollama restart: unless-stopped volumes: ollama-models:env_file会自动读取当前目录下的.env文件,把里面的键值对注入容器的环境变量。这样 compose 文件里不出现任何明文密钥,.env单独维护,并且加入.gitignore。
depends_on只是控制启动顺序,不保证 ollama 服务已经准备好了。如果 OpenClaw 启动太快,连不上 ollama,可以配合 healthcheck 或者让 OpenClaw 支持重试。我在实际使用中,习惯在 OpenClaw 的配置里把 ollama 的 base_url 设为http://ollama:11434,容器间通过服务名直连,不用操心 IP 变化。
4. 密钥安全:让 API Key 不进镜像、不进代码、不进日志
4.1 环境变量注入的正确姿势
很多人第一次用 Docker 时,喜欢在 Dockerfile 里写ENV OPENAI_API_KEY=sk-xxxx,这是大忌。镜像是由一层层文件组成的,ENV指令会把密钥明文写进镜像层,别人拉取或导出镜像后,用docker history就能看到。
正确的姿势是运行时注入:
docker run -e OPENAI_API_KEY="$(cat ~/.openai_key)" ...或者写进.env,再通过env_file让 Compose 读取。
可能有人问:构建的时候怎么办?有些 Python 包安装时需要 API Key 作为凭据,这时候要用 BuildKit 的--secret特性,而不是ARG。因为ARG也会被docker history记录。使用方式:
docker build --secret id=apikey,src=./.env .Dockerfile 里:
RUN --mount=type=secret,id=apikey eval "$(cat /run/secrets/apikey)"这样密钥只存在于构建时的临时挂载文件里,不会留进镜像层。实测这个功能非常实用,尤其是个别代理需要调用私有源拉依赖包时。
4.2 .env 文件与 Docker Secrets 怎么选
我现在的习惯是分环境:
- 开发环境用
.env文件。简单直接,Compose 自动读取,改完重启容器就生效。 - 生产环境用 Docker Secrets,或者干脆用服务器上的密钥管理服务。
.env文件注意三点:
- 文件权限设为
600:chmod 600 .env - 加入
.gitignore:echo ".env" >> .gitignore - 文件名建议用
.env而不是.env.local,因为 Compose 默认只读取.env
Docker Secrets 的用法也不复杂,适合单个密钥:
printf "sk-xxxx" | docker secret create openai_key -然后在 compose 文件里声明:
services: openclaw: image: openclaw-custom:latest secrets: - openai_key secrets: openai_key: external: true容器内密钥会被挂载到/run/secrets/openai_key文件,你的应用需要主动读取这个文件。OpenClaw 如果不原生支持读取 secret 文件,你可能要写一个小包装入口,把文件内容读到环境变量后再启动。
4.3 挂载目录的权限设计
容器里的数据卷最怕权限乱。我之前用 root 用户跑容器,结果在宿主机上留下的数据文件全部属于 root,普通用户想删都删不掉。后来统一用命名卷 + 非 root 用户,问题干净解决。
如果你挂载了一个宿主机目录到容器里,比如./data:/app/data,建议在容器启动时指定用户 UID:
docker run -u $(id -u):$(id -g) -v ./data:/app/data ...Compose 里也可以在服务下写user: "1000:1000"。
还有一个容易忽略的地方:不要把宿主机的~/.ssh、/etc/passwd、/var/run/docker.sock随便挂载进容器。有些人为了让 OpenClaw 能操作宿主机 Docker,直接挂载/var/run/docker.sock,这等于给了容器宿主机 root 权限,极其危险。如果一定要联动宿主机服务,优先考虑用 API 或网络接口,而不是给 socket 权限。
5. 沙盒内 OpenClaw 的日常指挥:启动、升级、排障
5.1 容器生命周期管理命令
容器跑起来之后,日常用到的命令其实就那几个。我把自己的高频命令列一下:
# 查看容器状态 docker ps -a | grep openclaw # 看日志(最常用) docker logs -f openclaw # 进入容器调试 docker exec -it openclaw bash # 重启容器 docker restart openclaw # 停用并删除 docker rm -f openclaw需要注意docker logs -f会显示容器内 stdout 和 stderr。如果你发现日志里有打印完整环境变量或密钥内容的情况,一定要在 OpenClaw 的日志级别里关掉配置打印。比如把 log 等级调到warning,或者用过滤器脱敏。我早期就被这条坑过,后来加了日志脱敏插件才算放心。
进入容器后,可以手动执行 OpenClaw 的 CLI 命令。比如查看技能列表:
docker exec -it openclaw openclaw skill list更新某个技能:
docker exec -it openclaw openclaw skill update skill-name这些操作不会影响宿主机环境,改坏了直接docker rm -f重新跑一个,根本不用怕。
5.2 如何验证你的 API 密钥真的“没裸奔”
部署完不能光看能跑就说安全,我一般做三遍检查。
第一遍,检查镜像历史:
docker history --no-trunc openclaw-custom:latest | grep -i "api_key\|sk-" || echo "镜像层中没有发现密钥"如果输出为空,说明构建层没泄。
第二遍,检查环境变量是否被写入配置文件。进入容器:
docker exec openclaw cat /app/config.yaml | grep "api_key"我期望看到的是类似${OPENAI_API_KEY}的引用,而不是明文。
第三遍,把.env文件临时移到别处,重启容器:
mv .env /tmp/env.bak docker restart openclaw如果 OpenClaw 还能正常调用 API,说明密钥已经通过环境变量注入且没有回写文件;如果它启动报“缺少 API Key”,你得检查是不是某个启动脚本把环境变量重新写进了配置。
我自己的经验是,OpenClaw 的配置解析如果支持${ENV_VAR}语法,那就不用担心回写问题;如果不支持,你只能在入口脚本里做一个“从环境变量生成配置文件但不落盘”的临时方案,比如用/dev/shm。
5.3 数据持久化和镜像更新
容器最大的好处之一是升级方便。以前宿主机装新版 OpenClaw,我得先备份旧环境,再卸载重装,中间出了错还回不去。现在只需要改镜像 tag,然后:
docker compose pull openclaw docker compose up -d数据卷里的技能、配置、任务历史都在,容器替换不影响数据。
但注意:如果新版镜像改了数据目录结构,或者配置文件格式不兼容,直接升级可能报错。我的建议是升级前先备份数据卷:
docker run --rm -v openclaw-data:/data -v $(pwd):/backup alpine tar czf /backup/openclaw-backup.tar.gz -C /data .把备份文件下载到本地,再升级。如果出问题,回滚也就是重新指定老镜像版本。
6. 容器化后的实测体验和几个坑
6.1 网络模式:我为什么最后选了 bridge
一开始我图省事,直接用了--network host。在 Linux 上,host 模式让容器直接共享宿主机网络,端口不用映射,访问localhost:8080就能进 OpenClaw。但问题随之而来:host 模式下容器和宿主机之间的网络隔离基本失效,OpenClaw 一旦被攻破,它对本机其他服务的访问范围和宿主机进程一样大。这和“沙盒”初衷是矛盾的。
最后我换回默认 bridge 模式,用-p显式映射端口。容器内部通过http://172.17.0.1访问宿主机上的服务,通过http://ollama:11434访问 compose 里的其他容器。隔离更彻底,也没损失多少性能。
如果你在 Docker Desktop 上需要访问宿主机服务,直接用host.docker.internal域名。Linux 上要手动加一个:
docker run --add-host host.docker.internal:host-gateway ...Compose 里对应:
extra_hosts: - "host.docker.internal:host-gateway"6.2 资源限制:N100 小主机也能跑
我后来专门弄了一台 N100 小主机,软路由兼跑 Docker。原本担心容器化 OpenClaw 太吃资源,实测下来还好。OpenClaw 本身是一个 Python 应用,空载内存大约 200-400MB,加上本地 Ollama 跑小模型,俩容器总共吃 2GB 左右。
为了不让它把整台机器拖垮,我给 OpenClaw 做了资源限制:
services: openclaw: deploy: resources: limits: cpus: '1.0' memory: 1g这样即使某个技能陷入循环,也不会把 N100 的四个核心全部吃满。实际上在top里观察,OpenClaw 的 CPU 占用平时不到 5%,只有处理复杂任务时会短暂升到 50% 以上。容器化没带来明显的性能损失,但换来了“出了事不牵连宿主机”的安心。
6.3 和本地 Ollama 通信的曲折经历
最后说说和 Ollama 通信的坑。我在 compose 里同时跑了 OpenClaw 和 Ollama,OpenClaw 的配置里把模型地址指向http://ollama:11434。结果第一次调用一直超时,原因有两个。
第一个是启动顺序。Ollama 容器虽然先启动了,但模型加载需要时间,OpenClaw 连接时模型还没就绪。解决办法是给 Ollama 加 healthcheck:
healthcheck: test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"] interval: 10s retries: 5然后 OpenClaw 的depends_on加上condition: service_healthy。
第二个是容器内网络请求外网 API 时,如果模型调用走代理,OpenClaw 的 HTTP 客户端默认不读容器内环境变量的代理配置,得在 OpenClaw 的配置文件里显式设置http_proxy和https_proxy。不过为了密钥安全,API 请求建议走 HTTPS,避免中间人窃听。容器的网络出口我默认放行 443 端口,其余端口按需开。
还有一个冷知识:如果你把 OpenClaw 容器和 MongoDB 容器放在同一个 compose 网络中,网络名称默认是项目名_默认。在 OpenClaw 容器内,可以通过服务名访问 MongoDB,但如果你从宿主机直接访问容器 IP,可能会因为网络隔离而失败。刚开始排查问题时要分清楚是容器间通信还是宿主机到容器通信,别混在一起猜。
最后聊两句我的真实感受
做了这轮容器化改造之后,我最大的收获不是“跑得更快”,而是心态变了。以前改配置、加技能、升级版本,总怕把宿主机搞乱,现在随便折腾,删了重来。API 密钥也规矩地待在.env里,开机自动注入,关机不留痕迹。如果你想在安卓手机上用 Termux 部署 OpenClaw,其实一样可以用 Docker 的思路,只不过 Termux 里的 Docker 兼容性相对折腾,我更推荐在局域网里放一台小主机专门跑容器。
如果你还没迁移,我给你的建议很简单:从 Compose 开始,哪怕只有一个服务,也先把.env和docker-compose.yml分好层。等哪天真出了安全事故,你会发现这一步省下的不只是一张账单。