1. 为什么 Docker Compose 部署 OpenClaw 时密钥总是注入失败
如果你正在用 Docker Compose 跑 OpenClaw,并且打算用 1Password 来托管 DeepSeek API 密钥,那你大概率会遇到下面这几类报错:容器启动几秒就退出、日志里写着apiKey is empty、Model is not allowed,或者配置向导把 DeepSeek Chat 错误地挂到了 Anthropic 提供方下面。这些问题的根源往往不在 OpenClaw 本身,而在于密钥从 1Password 到容器环境变量这条链路上,某一环断了。
OpenClaw 是一个可以本地部署、通过网关暴露能力的智能体框架,它支持 OpenAI 兼容协议,所以接 DeepSeek API 是可行的。DeepSeek 提供deepseek-chat和deepseek-reasoner两个主力模型,走的是 OpenAI 兼容的/v1接口。问题在于,很多人把密钥直接写进docker-compose.yml或者.env,既不安全,又容易在 Compose 变量替换时踩坑。1Password 的 CLI 工具op可以在启动容器前把密钥注入环境变量,做到密钥不落盘,但前提是注入命令、Compose 变量引用、容器内读取这三步必须对齐。
这篇内容面向的是已经会用 Docker Compose、但对 1Password 注入流程不熟、或者注入后仍然报错的开发者。我会给出可复制的docker-compose.yml和config.toml骨架,然后逐条验证:先确认 1Password CLI 注入结果,再确认容器内环境变量,最后复现并定位报错日志。整套流程我自己在本地和一台测试机上跑过,踩过的坑会直接标出来。
2. TaoToken 前置:先把 DeepSeek 密钥和接入信息准备好
在讲 1Password 注入之前,得先有一个可用的 DeepSeek API 密钥,以及一个稳定的接入地址。如果你还没有密钥,或者想用统一的网关来管理多个模型的调用,可以先去 TaoToken 的控制台创建一个 API Key。TaoToken 的 API 地址是https://taotoken.net/api,它兼容 OpenAI 协议,所以 OpenClaw 里配置openai-completions协议时可以直接指向它。
具体操作上,你可以打开 TaoToken 控制台 生成一个 Key,然后在 API Keys 页面 管理你的密钥。如果你只是想先验证模型能不能通,可以用 模型对话 页面直接发一条消息测试。对于长期跑编码任务或者 Agent 的场景,Coding Plan 会更合适,因为它的额度策略偏向持续调用。
拿到 Key 之后,先别急着写进 Compose。你要做的是把这个 Key 存进 1Password 的一个条目里,比如命名为OpenClaw DeepSeek,字段名用credential或者自定义的api_key。后面op命令会通过op://引用格式来读取它。这一步的意义在于:密钥只存在于 1Password 的加密库里,Compose 文件和.env里都不出现明文。
注意:不要把密钥直接写进
docker-compose.yml的environment字段,也不要用echo打印出来。一旦写进文件,Git 提交或者镜像层里就可能残留。
3. 可复制配置:docker-compose.yml 与 config.toml 骨架
下面这份docker-compose.yml的核心思路是:用op run包裹docker compose命令,让 1Password CLI 在启动前把op://引用解析成真实环境变量,再传给容器。这样容器内拿到的就是已经注入好的值。
services: openclaw: image: openclaw/openclaw:latest container_name: openclaw restart: unless-stopped ports: - "18789:18789" environment: - DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY} - OPENCLAW_CONFIG_DIR=/home/node/.openclaw volumes: - ./data:/home/node/.openclaw - ./data/conf:/home/node/.openclaw/conf command: ["node", "dist/index.js", "gateway", "--config", "/home/node/.openclaw/conf/config.toml"]这里的关键是DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY}。这个变量不会从.env文件读,而是由op run在运行时注入。你启动容器的命令应该是:
op run --env-file=./op.env -- docker compose up -d其中op.env文件内容长这样:
DEEPSEEK_API_KEY=op://Private/OpenClaw DeepSeek/credentialop://后面的路径对应你在 1Password 里的条目和字段。op run会解析这个引用,把真实密钥作为环境变量传给后面的docker compose进程,Compose 再把它传给容器。整个过程密钥不会出现在 shell 历史里,也不会写进磁盘。
接下来是config.toml骨架。OpenClaw 的配置格式在不同版本间有差异,有的版本用 JSON,有的用 TOML。这里给一份 TOML 版本,重点是把baseUrl写成带/v1的完整路径,并且apiKey用环境变量占位符。
[models] mode = "merge" [models.providers.deepseek] baseUrl = "https://api.deepseek.com/v1" apiKey = "${DEEPSEEK_API_KEY}" api = "openai-completions" [[models.providers.deepseek.models]] id = "deepseek-chat" name = "DeepSeek Chat" reasoning = false input = ["text"] contextWindow = 128000 maxTokens = 8192 [[models.providers.deepseek.models]] id = "deepseek-reasoner" name = "DeepSeek Reasoner" reasoning = true input = ["text"] contextWindow = 128000 maxTokens = 8192 [agents.defaults.model] primary = "deepseek/deepseek-chat" fallbacks = [] [agents.defaults] workspace = "/home/node/.openclaw/workspace" [gateway] port = 18789 mode = "local" bind = "lan" [gateway.auth] mode = "token" token = "${OPENCLAW_GATEWAY_TOKEN}"注意baseUrl这里写的是https://api.deepseek.com/v1,而不是https://api.deepseek.com。少了/v1会导致端点探测失败,日志里会出现 404 或者unsupported protocol。另外apiKey用${DEEPSEEK_API_KEY}占位,OpenClaw 启动时会从环境变量里读。如果你用的是 TaoToken 的网关,把baseUrl换成https://taotoken.net/api/v1即可,协议仍然是openai-completions。
4. 逐条验证:从 1Password 注入到容器内环境变量
配置写完之后,不要直接docker compose up,按下面四步逐条验证,能省掉大量排查时间。
第一步,验证 1Password CLI 能不能读到密钥。执行:
op read "op://Private/OpenClaw DeepSeek/credential"如果返回一串sk-开头的字符串,说明 1Password 条目和字段路径没问题。如果报item not found,检查条目名称和字段名是否和op.env里写的一致。注意字段名是区分大小写的。
第二步,验证op run注入结果。执行:
op run --env-file=./op.env -- env | grep DEEPSEEK_API_KEY你应该看到DEEPSEEK_API_KEY=sk-...。如果这里输出为空,说明op.env的格式有问题,或者op run没有正确解析。常见错误是op.env里写了引号,比如DEEPSEEK_API_KEY="op://...",引号会让op把它当成普通字符串而不是引用。
第三步,启动容器后确认容器内环境变量。执行:
docker compose exec openclaw env | grep DEEPSEEK_API_KEY如果容器已经退出,用docker compose run --rm openclaw env | grep DEEPSEEK_API_KEY来检查。这一步能确认变量是否真的传进了容器。如果宿主机有、容器里没有,检查docker-compose.yml的environment字段有没有写对变量名。
第四步,复现并定位报错日志。执行:
docker compose logs --tail=100 openclaw重点看有没有apiKey is empty、Model is not allowed、ECONNREFUSED这几类关键词。apiKey is empty说明环境变量没读到;Model is not allowed说明agents.defaults.models白名单里没有把deepseek/deepseek-chat加进去;ECONNREFUSED通常是baseUrl写错或者网络不通。
5. 本篇常见错排查:五类高频报错与对应修法
5.1 容器启动即退出,日志显示 apiKey is empty
这是最常见的一类。原因通常是op run没有包裹docker compose命令,或者op.env文件路径不对。检查你启动容器的命令是不是op run --env-file=./op.env -- docker compose up -d。如果你用的是docker compose up -d直接启动,那${DEEPSEEK_API_KEY}会从 shell 环境或者.env文件读,而这两处都没有值,容器自然拿不到密钥。
另一个可能是config.toml里apiKey写成了${DEEPSEEK_API_KEY},但 OpenClaw 的版本不支持这种占位符语法。这种情况下,改成从环境变量读取的写法,或者确认你的 OpenClaw 版本是否支持${}插值。如果不支持,可以在docker-compose.yml里直接把DEEPSEEK_API_KEY传给容器,然后config.toml里留空,让 OpenClaw 自动从环境变量读。
5.2 配置向导把 DeepSeek Chat 关联到 Anthropic 提供方
这个报错在通过docker compose run --rm openclaw-cli configure跑配置向导时特别容易出现。原因是向导在探测模型兼容性时,可能因为baseUrl缺少/v1或者协议字段没写对,误判了提供方类型。修法很简单:不要依赖向导自动写入,直接编辑data/conf/openclaw.json或者config.toml,手动把deepseek提供方的api字段设为openai-completions,baseUrl设为https://api.deepseek.com/v1。
如果你用的是 JSON 配置,结构大概是:
{ "models": { "mode": "merge", "providers": { "deepseek": { "baseUrl": "https://api.deepseek.com/v1", "apiKey": "${DEEPSEEK_API_KEY}", "api": "openai-completions", "models": [ { "id": "deepseek-chat", "name": "DeepSeek Chat", "reasoning": false, "input": ["text"], "contextWindow": 128000, "maxTokens": 8192 } ] } } } }改完之后重启容器:docker restart openclaw。注意容器名要换成你自己的,比如1Panel-openclaw-uTlW这种。
5.3 Model is not allowed 报错
这个报错和密钥无关,是agents.defaults.models白名单的问题。如果你在配置里定义了agents.defaults.models对象,那么智能体只能使用这个列表里列出的模型。如果列表里只有openrouter/deepseek/deepseek-chat,而你的primary写的是deepseek/deepseek-chat,切换模型时就会报Model is not allowed。
修法有两种:要么把deepseek/deepseek-chat加进白名单,要么直接删掉agents.defaults.models这个对象,禁用白名单限制。推荐后者,简单直接。
5.4 1Password CLI 报 permission denied 或 not signed in
op命令需要你先登录并授权。如果你在 CI 或者无头环境里跑,需要用OP_SERVICE_ACCOUNT_TOKEN来做服务账号认证。本地开发的话,先执行op signin完成登录。如果报permission denied,检查 1Password 条目所在的保险库是否对当前账号可见。
5.5 容器内能读到密钥但请求仍然 401
如果env | grep DEEPSEEK_API_KEY有值,但请求 DeepSeek API 返回 401,先检查密钥本身是否有效。可以用 curl 直接测:
curl https://api.deepseek.com/v1/chat/completions \ -H "Authorization: Bearer $DEEPSEEK_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"deepseek-chat","messages":[{"role":"user","content":"hi"}]}'如果 curl 也返回 401,说明密钥过期或者被撤销,去控制台重新生成一个。如果 curl 正常但 OpenClaw 报 401,检查config.toml里apiKey的占位符有没有被正确替换,有时候 TOML 解析器会把${DEEPSEEK_API_KEY}当成字面量而不是变量。
6. 接入验证与后续操作入口
配置改完、容器重启之后,验证是否真的通了。最直接的方式是看日志里有没有成功的模型调用记录,或者用 OpenClaw 的网关接口发一条测试消息。如果你只是想快速确认模型能不能响应,可以用 模型对话 页面发一条消息,对比返回结果和 OpenClaw 日志里的请求记录。
如果你在排障过程中需要重新生成密钥或者检查额度,去 API Keys 页面 操作。接入文档在 doc 里,里面有不同协议的 baseUrl 写法和参数说明。对于长期跑编码任务或者 Agent 的场景,Coding Plan 的额度策略更适合持续调用,不用频繁换 Key。
最后提醒一句:每次改完config.toml或者docker-compose.yml,都要重新走一遍op run启动流程,不要直接docker restart,因为restart不会重新注入环境变量。正确的重启方式是先docker compose down,再op run --env-file=./op.env -- docker compose up -d。这样能保证密钥始终从 1Password 读取,而不是残留在旧容器的环境里。