在 Hugging Face Spaces 上部署 9Router,跑了没多久就被封了:容器 PAUSED,错误写着 Flagged as abusive,域名访问429,点Restart this Spaces按钮出现503错误码。
一天之后它恢复正常,构建时间还从分钟级掉到十几秒。这篇把根因、可直接抄的 Dockerfile、Secrets 配置和日志判读方法一次性写完。
先说现场:被封时长这样
Space 当前状态:
Stage: PAUSED(已暂停)
Error: Flagged as abusive
Abuse 原因: NightWatch threat: Multiple commands executed by
Next.js descendant (severity: HIGH)
我第一反应是不服气。我什么坏事都没干,凭什么别人在 HF 上跑 9Router 一点事没有,我的就被判滥用了?
查了一圈才想明白:NightWatch 盯的不是我做了什么,是我怎么部署运行的。它是 Hugging Face 内部的自动滥用检测,判的是行为特征,不是你的动机。
一、你的部署姿势,在 NightWatch 眼里就是高危信号
把我和一个「一直 RUNNING、从没被封」的同类 Space 放在一起比,差别很直观:
| 维度 | 我的(被封) | 参考方(稳定) |
|---|---|---|
| 镜像来源 | 自己 clone 源码 + npm build | 官方预构建 decolua/9router:latest |
| 运行机制 | Next.js next start,裸 shell 进程 | 官方 custom-server.js,已有 handler / executor 封装 |
| 数据 | SQLite + 自写 hf buckets sync 脚本 | 官方 vault(Supabase),自动更新 |
| 权限 | root + 裸 node | entrypoint 降权(node:node + su-exec) |
| NightWatch 眼里 | 自己 build、裸进程、无封装 → 可疑 | 官方镜像、受控 executor、降权 → 相对可信 |
一句话总结:一边是「自己 build、裸进程、无封装」,一边是「官方镜像、受控 executor、降权运行」。NightWatch 会先盯哪一个,没有悬念。
Hugging Face Spaces 说到底是个托管演示环境。规则就一条:给你一个容器跑模型 demo,但别拿它当自主服务器,在里面执行任意 shell 命令。
偏偏 9Router 不是 demo。官方自己的描述很直白:把 Claude Code、Codex、Cursor、Cline 这些 AI 编码工具连到一个 OpenAI 兼容端点,40+ 提供商、100+ 模型统一路由。它天生是个代理,天生就得干系统级的活。
再配上我当时那版 Dockerfile:
FROM node:24-bookworm-slim
RUN git clone https://github.com/decolua/9router.git ... # 从源码拉
RUN npm install && npm run build # 全量编译
CMD ["/opt/9router-sync/start.sh"] # 裸跑 Next.js
这几件事叠在一起,正好凑齐了 NightWatch 要抓的特征组合:
- 从 GitHub clone 第三方源码并执行
- npm 全量 build(一次完整编译动作)
- 裸跑 Next.js 子进程
- root 权限 + 自己写的 shell 备份脚本(hf buckets sync)
- 日志里出现过它执行 shell 命令的记录
判定结果就是一句:「这容器在拿自己当服务器跑任意代码」 → 标记 → 封停。
二、翻了一个活着的同类,找到三招
同样是 9Router,它一直 RUNNING。我把它的 Dockerfile 翻了个底朝天,所谓的免死金牌就三招。
招一:用官方预构建镜像,绝不从源码 build
它的 Dockerfile 只有一行:
FROM decolua/9router:latest
应用运行时已经由发行商在镜像里编译好了,你拿过来直接用。没有 clone,没有 npm install,没有 npm run build —— 这些「构建时执行第三方代码」的动作一个都不存在,NightWatch 自然无从下手。
附带红利:构建从「分钟级 + 偶发 OOM」直接掉到十几秒。我自己改成这套之后,构建快到肉眼可见。
招二:套一层干净的 boot 编排,降权运行
它用 hf-entrypoint.sh 做启动编排,最后一步调用官方镜像自带的 /entrypoint.sh,通过 su-exec 从 root 降权到普通 node 用户再启动应用。不裸跑 root,也不开任意 shell。
招三:持久化用纯 Node,不碰 shell 脚本
备份 / 还原 SQLite 全部走纯 Node 脚本(VACUUM INTO 快照、加密、上传),而不是 hf buckets sync 这种 shell 命令。空闲时静默跳过,只有配置真的变了才上传。省流量,也避开 shell 检测。
三、重构后的 Dockerfile,可以直接抄
# 用官方预构建镜像 —— 不 clone、不 build
FROM decolua/9router:latest
ENV DATA_DIR=/data
USER root
COPY hf-samesite-patch.cjs /usr/local/bin/hf-samesite-patch.cjs
COPY hf-vault.mjs /usr/local/bin/hf-vault.mjs
COPY hf-vault-hydrate.mjs /usr/local/bin/hf-vault-hydrate.mjs
COPY hf-vault-backup.mjs /usr/local/bin/hf-vault-backup.mjs
COPY hf-vault-supervisor.mjs /usr/local/bin/hf-vault-supervisor.mjs
COPY hf-vault-mirrors.mjs /usr/local/bin/hf-vault-mirrors.mjs
COPY hf-sqlite-recover.mjs /usr/local/bin/hf-sqlite-recover.mjs
COPY hf-entrypoint.sh /usr/local/bin/hf-entrypoint.sh
RUN chmod 755 /usr/local/bin/hf-entrypoint.sh \
/usr/local/bin/hf-sqlite-recover.mjs \
/usr/local/bin/hf-vault.mjs \
/usr/local/bin/hf-vault-hydrate.mjs \
/usr/local/bin/hf-vault-backup.mjs \
/usr/local/bin/hf-vault-supervisor.mjs \
/usr/local/bin/hf-vault-mirrors.mjs \
&& chmod 644 /usr/local/bin/hf-samesite-patch.cjs
ENTRYPOINT ["/usr/local/bin/hf-entrypoint.sh"]
CMD ["node", "custom-server.js"]
README 的 frontmatter 里还有一个关键点:
sdk: docker
app_port: 20128 # ← 9Router 官方端口,别用 7860
容器完整部署需要的文件有如下图所示:

⚠️ app_port 必须跟官方镜像一致,填 20128。HF 会自动把外部 7860 映射过去,填错端口页面直接打不开。
四、配置数据备份:容器一重启,配置就归零
这是 HF 的常识坑:容器一重启,之前在面板里配的东西全部清空。所以持久化必须做。
这里的做法是把 SQLite 快照加密后存到免费的 Supabase 数据库,读写全走 Node 脚本,彻底不用 hf buckets sync。启动时 hydrate 拉回,运行中定期 backup 上传。
五、Secrets 配置,坑最集中的地方
部署到 HF Space 后,在 Settings → Secrets 里配这几个环境变量:
| Secret | 填什么 | 说明 |
|---|---|---|
INITIAL_PASSWORD | 你自己定的密码 | 后台首次登录用,不设默认是 123456 |
JWT_SECRET | openssl rand -base64 32 生成 | 登录 cookie 的签名密钥,不设则每次容器重启都要重新登录 |
SUPABASE_URL_1 | Supabase Project URL | 金库的存储后端地址 |
SUPABASE_SERVICE_ROLE_KEY_1 | Supabase 的 service_role | 金库的读写凭证,注意不是 anon |
VAULT_ENCRYPTION_KEY | openssl rand -base64 32 生成 | 加密金库的那把锁 |
四个最容易踩的坑
- JWT_SECRET 不等于 Supabase 的 JWT Keys。Supabase 后台那个 JWT Keys 是给它自己的 Auth 用的,跟 9Router 无关。你要的是
openssl rand -base64 32生成的对称密钥,只存在 HF Secret 里。落到 Supabase 手上,加密等于白做。 - service_role 在 Settings → API Keys 页。别手滑选成 anon,权限差很多,anon 写不进去。
- SUPABASE_URL_1 别带
/rest/v1/。脚本会自动拼路径,你多填一截就变成.../rest/v1//rest/v1/,直接 404。填到.supabase.co为止。 - LEGACY_DATA_DIR 不用填,默认就是
/data。

六、跑通了,日志长这样
构建启动后,日志里出现这几行,说明全部到位:
[hf-entrypoint] Vault configured; using fast local
runtime dir /tmp/ninerouter-data
[hf-vault-supervisor] Vault enabled. Periodic backup
every 600s
✓ Ready (Next.js 16.3.3) — port 20128
三点判读
egress today 0.00 MB 是黄金状态,不是故障。它只统计「从 Supabase 读出来的流量」。备份走的是上传,属于 ingress,免费无限。空间没重启、配置没变,读取路径根本不触发,所以是精准的 0。
600s 的定期备份不是每 5 分钟白传一次。内部有 stat 和 checksum 两级检测,没变化就静默跳过,零 SQLite 访问、零 egress。只有配置真变了 —— 你改密码、加 provider —— 才会真正上传。
只有重启时可能看到一次 egress > 0。那是 hydrate 从金库拉快照还原,几 MB,同样是健康信号。
七、老实说,这套到底稳不稳
必须坦诚:这套重构大幅降低了被 NightWatch 封停的概率(官方镜像 + 降权 + 无 shell),我这边已经稳定运行。但不敢说 100% 永不封。
原因很实在:9Router 本质是代理 Claude Code / Codex 这类工具的应用,这在 HF 的规则里本身就是高压区。官方镜像的封装能压低风险,压不到零。
如果你的目标是长期稳定跑全功能,我的建议是:
- 自己的 VPS / Docker,彻底没有 NightWatch 这一层
- Huggingface目前存量免费的dockerfile被封,想删掉重建可是要收费的了!
一句话总结
在 HF Spaces 上跑 9Router 这类面板:用官方预构建镜像,套一层干净的降权编排,持久化走纯 Node。别从源码 build,别裸跑 root shell。构建快到十几秒,也不容易被封。


暂无评论
要发表评论,您必须先 登录