1. 为什么我最后把 OpenClaw 塞进了一个 Dockerfile
OpenClaw(小龙虾)是一个可以自己部署的 AI 助手网关,能接兼容 OpenAI 协议的模型服务,然后在浏览器里对话、跑 Agent、挂 Telegram 通道。它适合两类人:一类是想有个自己的 AI 控制台、不想被某个客户端绑死;另一类是手里已经有模型 API,想找个统一入口把对话、编码、自动化都接进去。
问题出在部署环节。Hugging Face Spaces 的免费 CPU Basic 实例给 2 vCPU、16 GB 内存、100 GB 磁盘,听起来够用,但它有两个坑:一是容器重启后本地文件全丢,二是免费实例对部分外部域名的解析不稳定。我一开始按官方思路拆了sync.py、start-openclaw.sh、requirements.txt、Nginx 配置四五个文件,结果每次改一个环境变量就要动三处,备份恢复还经常对不上。
后来我把所有逻辑压进一个 Dockerfile:装依赖、生成同步脚本、生成启动脚本、修 DNS、恢复备份、定时备份、拉起 gateway,全在构建和启动阶段自动完成。你只需要在 Space 仓库根目录放一个Dockerfile,再配几个环境变量,就能跑起来。这篇就把这个单文件方案完整拆给你,包括怎么把模型请求统一接到 TaoToken 的 Key/API 通道上。
2. 部署前先把 TaoToken 的接入信息准备好
OpenClaw 本身不带模型,它需要一个兼容 OpenAI 协议的服务地址、一个 API Key、一个模型 ID。我这边统一走 TaoToken 的 API 通道,好处是后面换模型只改一个MODEL变量,不用动 Dockerfile。
你需要提前拿到三样东西:
- API Base:
https://taotoken.net/api - API Key:在控制台里生成,形如
sk-开头的一串 - 模型 ID:填你实际要用的那个,比如某个对话模型或编码模型的名字
生成 Key 的入口在这里,登录后进 API Keys 页面新建即可:
https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=dockerfile_openclaw
如果你还没想好接哪个模型,可以先在模型对话页面试一条请求,确认 Key 能用、模型名没写错,再回来配 Space:
https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=dockerfile_openclaw
接口文档在这里,主要看 base URL 和鉴权头的写法,OpenClaw 内部就是按这套协议发请求的:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=dockerfile_openclaw
这里有个容易踩的点:OPENAI_API_BASE到底填https://taotoken.net/api还是带/v1。OpenClaw 的配置里会把它当作 OpenAI 兼容根地址,实际请求路径是它自己拼的。我实测下来填https://taotoken.net/api就能通,如果你填了带/v1的版本反而可能拼出双/v1。拿不准就先按不带/v1的填,报 404 再调。
3. 单个 Dockerfile 的完整骨架
下面这份就是我要你复制进 Space 仓库根目录的内容。它做了几件事:基于node:22-slim装 Node、Python、Git、编译工具和网络诊断工具;装huggingface_hub用于备份同步;全局装openclaw@latest;在镜像里生成sync.py和start-openclaw两个脚本;启动时覆盖 DNS、从 Dataset 恢复最近备份、生成初始配置、每 15 分钟全量备份、收到终止信号再备份一次。
FROM node:22-slim ENV DEBIAN_FRONTEND=noninteractive ENV HOME=/root ENV PORT=7860 RUN apt-get update && apt-get install -y --no-install-recommends \ python3 python3-pip git openssh-client build-essential \ curl dnsutils iputils-ping ca-certificates \ && rm -rf /var/lib/apt/lists/* RUN pip3 install --no-cache-dir --break-system-packages huggingface_hub RUN npm install -g openclaw@latest # 内嵌备份/恢复脚本 RUN cat > /usr/local/bin/sync.py <<'PY' import os, sys, glob, tarfile, datetime from huggingface_hub import HfApi, hf_hub_download STATE_DIR = "/root/.openclaw" DATASET = os.environ.get("HF_DATASET", "") TOKEN = os.environ.get("HF_TOKEN", "") KEEP_DAYS = 30 def backup(): if not DATASET or not TOKEN: print("--- [SYNC] 缺少 HF_DATASET 或 HF_TOKEN,跳过备份") return api = HfApi(token=TOKEN) stamp = datetime.date.today().isoformat() name = f"backup_{stamp}.tar.gz" tmp = f"/tmp/{name}" with tarfile.open(tmp, "w:gz") as tar: if os.path.isdir(STATE_DIR): tar.add(STATE_DIR, arcname=".openclaw") api.upload_file(path_or_fileobj=tmp, path_in_repo=name, repo_id=DATASET, repo_type="dataset") print(f"--- [SYNC] 备份完成: {name}") def restore(): if not DATASET or not TOKEN: print("--- [SYNC] 缺少 HF_DATASET 或 HF_TOKEN,跳过恢复") return api = HfApi(token=TOKEN) files = api.list_repo_files(repo_id=DATASET, repo_type="dataset") backups = sorted([f for f in files if f.startswith("backup_") and f.endswith(".tar.gz")]) if not backups: print("--- [SYNC] 未发现备份文件,按首次运行处理") return latest = backups[-1] print(f"--- [SYNC] 发现备份文件: {latest}") path = hf_hub_download(repo_id=DATASET, filename=latest, repo_type="dataset", token=TOKEN) try: with tarfile.open(path, "r:gz") as tar: tar.extractall("/root") print("--- [SYNC] 恢复成功") except Exception as e: print(f"--- [SYNC] 解压失败: {e}") if __name__ == "__main__": if sys.argv[1] == "backup": backup() elif sys.argv[1] == "restore": restore() PY # 内嵌启动脚本 RUN cat > /usr/local/bin/start-openclaw <<'SH' #!/bin/bash set -e echo "--- [DNS] 覆盖 DNS 配置 ---" echo "nameserver 223.5.5.5" > /etc/resolv.conf echo "nameserver 8.8.8.8" >> /etc/resolv.conf echo "--- [SYNC] 启动恢复流程 ---" python3 /usr/local/bin/sync.py restore || true if [ -z "$OPENAI_API_KEY" ]; then echo "OPENAI_API_KEY is required"; exit 1 fi if [ -z "$MODEL" ]; then echo "MODEL is required"; exit 1 fi CONF=/root/.openclaw/openclaw.json mkdir -p /root/.openclaw if [ ! -f "$CONF" ]; then echo "--- [INIT] 生成初始配置 ---" if [ -z "$OPENCLAW_GATEWAY_PASSWORD" ]; then OPENCLAW_GATEWAY_PASSWORD=$(head -c 12 /dev/urandom | base64 | tr -d '/+=') echo "--- [INIT] Gateway 密码: $OPENCLAW_GATEWAY_PASSWORD" fi echo "$OPENCLAW_GATEWAY_PASSWORD" > /root/.openclaw/gateway.passwd cat > "$CONF" <<JSON { "gateway": { "password": "$OPENCLAW_GATEWAY_PASSWORD" }, "models": { "default": { "baseUrl": "$OPENAI_API_BASE", "apiKey": "$OPENAI_API_KEY", "model": "$MODEL" } } } JSON fi # 后台定时备份 ( while true; do sleep 900; python3 /usr/local/bin/sync.py backup || true; done ) & trap 'python3 /usr/local/bin/sync.py backup || true' TERM INT openclaw doctor --fix || true exec openclaw gateway run --port "$PORT" SH RUN chmod +x /usr/local/bin/start-openclaw EXPOSE 7860 CMD ["/usr/local/bin/start-openclaw"]这份文件里没有单独的requirements.txt,Python 依赖直接pip3 install;没有单独的 Nginx 配置,OpenClaw 自己监听7860;没有单独的sync.py文件,它是在构建阶段用 heredoc 写进镜像的。你维护的永远只有这一个 Dockerfile。
4. Space 环境变量与 settings.json 接入片段
Dockerfile 提交后,去 Space 的 Settings 页面,在 Variables and secrets 里加下面这些。敏感值放 Secrets,非敏感的放 Variables。
| 变量名 | 是否必填 | 说明 |
|---|---|---|
| OPENAI_API_BASE | 必填 | 填https://taotoken.net/api |
| OPENAI_API_KEY | 必填 | TaoToken 控制台生成的 Key,放 Secrets |
| MODEL | 必填 | 你要用的模型 ID |
| HF_DATASET | 必填 | 备份用的 Dataset,格式用户名/数据集名 |
| HF_TOKEN | 必填 | Hugging Face Write Token,放 Secrets |
| OPENCLAW_GATEWAY_PASSWORD | 建议填 | 登录 OpenClaw 页面的密码 |
| PORT | 可选 | 默认 7860,一般不用改 |
| TELEGRAM_BOT_TOKEN | 可选 | 不用 Telegram 就别填 |
如果你不想用 Dockerfile 里自动生成的openclaw.json,也可以手动写一份配置挂进去。OpenClaw 读的是/root/.openclaw/openclaw.json,接入 TaoToken 的关键片段长这样:
{ "gateway": { "password": "你的登录密码" }, "models": { "default": { "baseUrl": "https://taotoken.net/api", "apiKey": "sk-你的TaoToken密钥", "model": "你的模型ID" } } }注意baseUrl这里我填的是不带/v1的版本。如果你手动改配置后发现请求 404,先把baseUrl换成带/v1的再试一次,两种写法在不同 OpenClaw 版本里行为略有差异。改完配置记得重启 Space,因为启动脚本只在openclaw.json不存在时才生成初始配置,已存在的文件它不会覆盖。
5. 验证部署:发一次真实对话请求
构建完成后打开 Space 的 App 页面,地址形如https://你的空间名.hf.space。输入OPENCLAW_GATEWAY_PASSWORD登录。如果你没设密码,去 Logs 里搜--- [INIT] Gateway 密码:,那串就是。
登录后别急着配一堆东西,先做一次最小验证:在对话界面发一句「你好,用一句话介绍你自己」。这一步会真正打到 TaoToken 的接口上,能返回内容就说明baseUrl、apiKey、model三个值都对。
如果你想在容器里直接验证,可以进 Space 的终端跑一条 curl:
curl -s https://taotoken.net/api/chat/completions \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "'"$MODEL"'", "messages": [{"role": "user", "content": "ping"}] }'返回里带choices字段就说明 Key 和模型 ID 没问题。如果这里通、OpenClaw 页面不通,那问题在 OpenClaw 的配置读取上,不在网络。
验证完对话,再做一次备份闭环测试:随便改一个配置或建一个会话,等 15 分钟让定时备份跑一次,然后去 Dataset 里看有没有backup_日期.tar.gz。有文件就说明HF_DATASET和HF_TOKEN配对了。接着手动重启 Space,重新登录,看之前的会话还在不在。在,就说明恢复流程也通了。
6. 部署后常见报错排查
构建阶段npm install -g openclaw@latest失败。多半是网络波动,先重新触发一次构建。如果稳定复现,检查 Dockerfile 里FROM node:22-slim这行有没有被误改,以及apt-get update那一段有没有语法错误。构建日志会明确告诉你卡在哪一层。
页面打不开,日志里没有 gateway 启动信息。先看日志有没有OPENAI_API_KEY is required或MODEL is required。启动脚本在这两个变量缺失时会直接退出,不会拉起 gateway。补齐变量后重启 Space,再确认日志里出现openclaw gateway run --port。
能打开页面但登录不进去。如果你手动设了密码,确认输入值和 Secrets 里一致。如果没设,去日志找--- [INIT] Gateway 密码:。首次启动后这个密码会写进/root/.openclaw/gateway.passwd并随备份保存。如果重启后密码变了,说明备份没恢复成功,按下面那条处理。
重启后配置或会话丢失。先去 Dataset 看最近日期的backup_YYYY-MM-DD.tar.gz在不在。不在,说明定时备份没写进去,检查HF_DATASET和HF_TOKEN,尤其确认 Token 是 Write 权限。文件在但没恢复,看启动日志里有没有--- [SYNC] 发现备份文件和--- [SYNC] 恢复成功,根据报错定位。
模型调用失败。检查OPENAI_API_BASE、OPENAI_API_KEY、MODEL三个值。如果你改了环境变量但没删旧配置,OpenClaw 会继续用旧的openclaw.json。确认要换配置的话,先备份当前状态,删掉/root/.openclaw/openclaw.json再重启,让脚本重新生成。
免费实例休眠导致掉线。这是 Hugging Face 免费层的机制,不是部署问题。你可以加一个定时轮询请求保持活跃,或者接受它偶尔休眠、靠备份恢复。要真正稳定在线,得升级硬件或换到常驻环境。
7. 接下来怎么把这套东西用顺
部署跑通之后,我建议先把MODEL固定下来,别频繁换,因为每次换都要重启 Space 重新生成配置。备份频率现在是 15 分钟一次、保留 30 天,如果你会话不多可以调长一点,减少 Dataset 写入。
如果你后面要长期跑编码任务或 Agent,可以看看 Coding Plan,它更适合持续性的模型调用场景:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=dockerfile_openclaw
需要管理多个 Key 或看调用量,去控制台:
https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=dockerfile_openclaw
这套单文件方案的价值在于:你只维护一个 Dockerfile,状态靠 Dataset 持久化,模型接入靠三个环境变量。等你把基础对话跑顺了,再往上加 Telegram 通道或远程访问,改动都集中在这一个文件里,不会像多文件方案那样牵一发动全身。