抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

最近折腾了一套把 CLIProxyAPICPA Manager Plus 部署到 Hugging Face Space 的方案。目标很明确:不买 VPS,尽量使用免费资源,把 API 网关、管理面板和用量统计放在同一个 Space 里运行,并且通过同一个域名访问。

这篇文章最早写的是 CPA + CPA-Manager 的同域部署。后来 CPA 的用量统计和管理面板能力有变化,我把方案更新成了 CLIProxyAPI + CPA Manager Plus。现在的部署方式还有一个小改动:容器启动时会自动拉取最新的 CPA 和 CPA Manager Plus 二进制,下载失败时再回退到镜像里内置的旧二进制,避免每次上游发版都要手动改 Dockerfile。

最终访问结构如下:

1
2
3
4
5
CPA 主服务:
https://username-space-name.hf.space/

CPA Manager Plus 管理面板:
https://username-space-name.hf.space/cpm/

为什么不能直接照搬 VPS 方案

在 VPS 上,常见做法是:

1
2
Docker Compose 跑 cli-proxy-api 和 cpa-manager-plus
宿主机 Caddy 负责 HTTPS 和反代

但是 Hugging Face Docker Space 的模型不一样。Space 对公网通常只暴露一个应用端口,默认是 7860。也就是说,不能像 VPS 一样直接把多个容器、多个端口都暴露出去。

所以 Space 里的方案要改成:

1
2
3
4
5
6
7
8
9
10
11
12
13
一个 Docker 容器
容器内同时启动:
- CLIProxyAPI
- CPA Manager Plus
- Caddy

公网只暴露:
- 7860

内部端口:
- 8317 -> CLIProxyAPI
- 18317 -> CPA Manager Plus
- 7860 -> Caddy 反代入口

路由关系如下:

1
2
3
4
5
6
7
8
/                    -> CLIProxyAPI
/v1/* -> CLIProxyAPI
/cpm/ -> CPA Manager Plus 管理面板
/health -> CPA Manager Plus
/status -> CPA Manager Plus
/setup -> CPA Manager Plus
/usage-service/* -> CPA Manager Plus
/v0/management* -> CPA Manager Plus,再由它按需要代理到 CPA

简单来说,外部只有一个入口 7860,Caddy 根据路径把请求转发给内部不同服务。/cpm/ 是管理面板入口,其他 OpenAI-compatible API 请求仍然走 CPA。

新建 Hugging Face Docker Space

进入:

1
https://huggingface.co/new-space

建议配置:

1
2
3
Space SDK: Docker
Visibility: Public 或 Private
Hardware: CPU Basic

Space 仓库里放这些文件:

1
2
3
4
5
6
7
Dockerfile
Caddyfile
config.yaml
start.sh
README.md
cli-proxy-api
cpa-manager-plus

其中 cli-proxy-apicpa-manager-plus 是兜底二进制。正常情况下,容器启动时会先从 GitHub Releases 拉最新版本;如果 GitHub 网络临时失败,才使用这两份内置文件。

Dockerfile

Dockerfile 只负责准备运行环境、复制兜底二进制和配置文件。不要在 Docker build 阶段去 ADD https://api.github.com/.../releases/latest,我实测 Hugging Face 的构建器可能会因为远程 ADD 的缓存处理失败,导致 Space 进入 BUILD_ERROR

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
FROM alpine:3.21

RUN apk add --no-cache bash ca-certificates caddy curl jq libc6-compat

WORKDIR /app

COPY cli-proxy-api /app/cli-proxy-api
COPY cpa-manager-plus /app/cpa-manager-plus
RUN chmod +x /app/cli-proxy-api /app/cpa-manager-plus

RUN mkdir -p /tmp/.cli-proxy-api /tmp/logs /tmp/pg_cache/pgstore /data \
&& chmod -R 777 /tmp /data

COPY config.yaml /app/config.yaml
COPY Caddyfile /app/Caddyfile
COPY start.sh /app/start.sh

RUN cp /app/config.yaml /app/config.example.yaml \
&& chmod +x /app/start.sh

ENV TZ=Asia/Shanghai
ENV PORT=7860
ENV HTTP_ADDR=0.0.0.0:18317
ENV USAGE_DATA_DIR=/data
ENV USAGE_DB_PATH=/data/usage.sqlite
ENV CPA_MANAGER_DATA_KEY_PATH=/data/data.key
ENV USAGE_COLLECTOR_MODE=auto
ENV USAGE_BATCH_SIZE=100
ENV USAGE_POLL_INTERVAL_MS=500
ENV USAGE_QUERY_LIMIT=50000
ENV USAGE_CORS_ORIGINS=*
ENV USAGE_RESP_QUEUE=usage
ENV USAGE_RESP_POP_SIDE=right

EXPOSE 7860

CMD ["/app/start.sh"]

这里有两个关键点。

第一,仍然保留:

1
RUN cp /app/config.yaml /app/config.example.yaml

CLIProxyAPI 启动时会读取这个模板文件,缺少它可能会报:

1
open /app/config.example.yaml: no such file or directory

第二,不建议把 CPA_MANAGER_ADMIN_KEY 写死在 Dockerfile 里。这个值相当于管理面板的管理员密码,应该放到 Space 的 Settings -> Variables and secrets 里。

config.yaml

CPA 不要直接监听 7860,否则会和 Caddy 冲突。这里让 CPA 监听内部端口 8317

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
host: "0.0.0.0"
port: 8317

pgstore-dsn: "${PGSTORE_DSN}"

auth-dir: "/tmp/.cli-proxy-api"
logging-to-file: true
logs-dir: "/tmp/logs"

remote-management:
enabled: true
secret-key: "${MANAGEMENT_PASSWORD}"

usage-statistics-enabled: true
redis-usage-queue-retention-seconds: 600

commercial-mode: true
debug: false

如果需要持久化配置,推荐使用外部 PostgreSQL,例如 Aiven、Supabase 等。连接串放到 Space Secret 里,不要写进仓库。

Caddyfile

Caddy 监听 7860,负责把外部请求分发给内部的 CPA 和 CPA Manager Plus。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
:7860 {
encode gzip zstd

redir /cpm /cpm/

handle /cpm/ {
rewrite * /management.html
reverse_proxy 127.0.0.1:18317 {
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}

handle_path /cpm/* {
reverse_proxy 127.0.0.1:18317 {
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}

handle /health {
reverse_proxy 127.0.0.1:18317
}

handle /status {
reverse_proxy 127.0.0.1:18317
}

handle /setup {
reverse_proxy 127.0.0.1:18317
}

handle /usage-service/* {
reverse_proxy 127.0.0.1:18317
}

handle /v0/management* {
reverse_proxy 127.0.0.1:18317
}

handle {
reverse_proxy 127.0.0.1:8317 {
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}
}

/cpm/ 单独 rewrite 到 /management.html 是为了避免 CPA Manager Plus 自己重定向到根路径后,被 Caddy 转发给 CPA,导致页面 404。

start.sh

启动脚本负责做三件事:

  1. 设置 CPA Manager Plus 连接 CPA 所需的环境变量。
  2. 启动时尝试下载最新版 CLIProxyAPICPA Manager Plus
  3. 同时拉起 CPA、CPA Manager Plus 和 Caddy。

核心逻辑如下,完整脚本可以直接放到 Space 仓库里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env bash
set -euo pipefail

export CPA_UPSTREAM_URL="${CPA_UPSTREAM_URL:-http://127.0.0.1:8317}"
export CPA_MANAGEMENT_KEY="${CPA_MANAGEMENT_KEY:-${MANAGEMENT_PASSWORD:-}}"
export HTTP_ADDR="${HTTP_ADDR:-0.0.0.0:18317}"
export USAGE_DATA_DIR="${USAGE_DATA_DIR:-/data}"
export USAGE_DB_PATH="${USAGE_DB_PATH:-/data/usage.sqlite}"
export CPA_MANAGER_DATA_KEY_PATH="${CPA_MANAGER_DATA_KEY_PATH:-/data/data.key}"
export USAGE_COLLECTOR_MODE="${USAGE_COLLECTOR_MODE:-auto}"
export CPA_VERSION="${CPA_VERSION:-latest}"
export CPA_MANAGER_PLUS_VERSION="${CPA_MANAGER_PLUS_VERSION:-latest}"

CPA_BIN="/app/cli-proxy-api"
CPAM_BIN="/app/cpa-manager-plus"

CPA_VERSIONCPA_MANAGER_PLUS_VERSION 默认都是 latest。如果你想临时锁定版本,比如回滚到某个稳定版本,可以在 Space 变量里设置:

1
2
CPA_VERSION=v7.1.54
CPA_MANAGER_PLUS_VERSION=v1.2.0

下载 latest 的思路是调用 GitHub Releases API,按当前 CPU 架构匹配 Linux 包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
download_release_asset() {
local repo="$1"
local version="$2"
local pattern="$3"
local output="$4"
local api_url asset_url

if [[ "${version}" == "latest" ]]; then
api_url="https://api.github.com/repos/${repo}/releases/latest"
else
api_url="https://api.github.com/repos/${repo}/releases/tags/${version}"
fi

if ! asset_url="$(curl -fsSL -H 'Accept: application/vnd.github+json' -H 'User-Agent: agri-gateway-runtime' "${api_url}" \
| jq -r --arg pattern "${pattern}" '.assets[] | select(.name | test($pattern)) | .browser_download_url' \
| head -n 1)"; then
echo "Failed to read release metadata from ${api_url}" >&2
return 1
fi

if [[ -z "${asset_url}" || "${asset_url}" == "null" ]]; then
echo "No release asset matched ${pattern} in ${repo}@${version}" >&2
return 1
fi

curl -fsSL "${asset_url}" -o "${output}"
}

CPA 和管理面板的下载都做成了“失败不退出”的兜底逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 每次容器启动先拉取 CPA 和管理面板的发布包;如果 GitHub 暂不可用,则继续使用镜像内置二进制,避免实验环境整体无法启动。
if download_release_asset "router-for-me/CLIProxyAPI" "${CPA_VERSION}" "CLIProxyAPI_.*_linux_${cpa_release_arch}\\.tar\\.gz$" "${release_dir}/cpa.tar.gz" \
&& tar -xzf "${release_dir}/cpa.tar.gz" -C "${release_dir}/cpa" \
&& cp "${release_dir}/cpa/cli-proxy-api" "${bin_dir}/cli-proxy-api" \
&& chmod +x "${bin_dir}/cli-proxy-api"; then
CPA_BIN="${bin_dir}/cli-proxy-api"
else
echo "Failed to download CPA ${CPA_VERSION}. Using bundled /app/cli-proxy-api." >&2
fi

# 管理面板和 Manager Server 同步更新;下载失败时保留旧版面板,保证 /cpm/ 入口仍可访问。
if download_release_asset "seakee/CPA-Manager-Plus" "${CPA_MANAGER_PLUS_VERSION}" "cpa-manager-plus_.*_linux_${cpam_release_arch}\\.tar\\.gz$" "${release_dir}/cpam.tar.gz" \
&& tar -xzf "${release_dir}/cpam.tar.gz" -C "${release_dir}/cpam" \
&& cp "$(find "${release_dir}/cpam" -type f -name cpa-manager-plus | head -n 1)" "${bin_dir}/cpa-manager-plus" \
&& chmod +x "${bin_dir}/cpa-manager-plus"; then
CPAM_BIN="${bin_dir}/cpa-manager-plus"
else
echo "Failed to download CPA Manager Plus ${CPA_MANAGER_PLUS_VERSION}. Using bundled /app/cpa-manager-plus." >&2
fi

最后启动三个进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"${CPA_BIN}" --config /app/config.yaml &
cpa_pid=$!

"${CPAM_BIN}" &
cpam_pid=$!

caddy run --config /app/Caddyfile --adapter caddyfile &
caddy_pid=$!

cleanup() {
kill "${cpa_pid}" "${cpam_pid}" "${caddy_pid}" 2>/dev/null || true
}
trap cleanup EXIT INT TERM

wait -n "${cpa_pid}" "${cpam_pid}" "${caddy_pid}"
exit_code=$?
cleanup
wait || true
exit "${exit_code}"

Space 环境变量

在 Space 的 Settings -> Variables and secrets 中配置:

1
2
3
4
5
6
7
PGSTORE_DSN                 Secret
MANAGEMENT_PASSWORD Secret
CPA_MANAGER_ADMIN_KEY Secret
MANAGEMENT_STATIC_PATH /tmp
PGSTORE_LOCAL_PATH /tmp/pg_cache
TZ Asia/Shanghai
PORT 7860

可选版本控制变量:

1
2
CPA_VERSION                latest
CPA_MANAGER_PLUS_VERSION latest

PostgreSQL 连接串示例:

1
postgres://username:password@host:port/defaultdb?sslmode=require

注意不要把真实连接串、管理密码、管理员 key 提交到 GitHub,也不要写进 README 或博客。

触发部署

如果使用 hf CLI,可以直接上传修改过的文件触发 Space rebuild:

1
2
3
4
5
hf upload username/api-gateway-education-lab . . \
--repo-type space \
--include Dockerfile \
--include start.sh \
--commit-message "Download latest CPA binaries at runtime"

查看状态:

1
hf spaces info username/api-gateway-education-lab --json

查看构建日志:

1
hf spaces logs username/api-gateway-education-lab --build --tail 80

查看运行日志:

1
hf spaces logs username/api-gateway-education-lab --tail 80

如果状态从 APP_STARTING 变成 RUNNING,基本就部署完成了。

启动验证

Space 构建完成后,可以访问:

1
https://username-space-name.hf.space/usage-service/info

如果 CPA Manager Plus 正常,会返回类似:

1
2
3
4
5
6
7
{
"service": "cpa-manager-plus",
"mode": "embedded",
"configured": true,
"adminReady": true,
"setupRequired": false
}

管理面板访问:

1
https://username-space-name.hf.space/cpm/

CPA OpenAI-compatible 接口访问:

1
https://username-space-name.hf.space/v1

如果访问 / 返回 404,不一定是部署失败。很多时候只是 CPA 根路径没有页面,应该重点测试 /cpm//usage-service/info/v1

Codex 接入 CPA Space

如果想让 Codex 默认走这个 Space,可以在 ~/.codex/config.toml 里添加自定义 provider:

1
2
3
4
5
6
7
8
9
10
11
12
model = "gpt-5.5"
model_provider = "cpa_space"

[model_providers.cpa_space]
name = "CPA Space"
base_url = "https://username-space-name.hf.space/v1"
wire_api = "responses"
env_key = "CPA_SPACE_API_KEY"

[profiles.cpa_space]
model_provider = "cpa_space"
model = "gpt-5.5"

然后设置环境变量:

1
2
[Environment]::SetEnvironmentVariable("CPA_SPACE_API_KEY", "your-cpa-api-key", "User")
$env:CPA_SPACE_API_KEY="your-cpa-api-key"

测试:

1
codex exec --ephemeral --skip-git-repo-check "Reply with exactly: OK"

如果返回 OK,说明 Codex 已经走到了你的 CPA Space。

常见问题

Space 进入 BUILD_ERROR

先看构建日志:

1
hf spaces logs username/api-gateway-education-lab --build --tail 120

如果你在 Dockerfile 里用了类似下面这种远程 ADD

1
ADD https://api.github.com/repos/router-for-me/CLIProxyAPI/releases/latest /tmp/cpa-latest-release.json

Hugging Face builder 可能会因为缓存处理失败而报错。更稳的做法是 Dockerfile 只复制兜底二进制,把 latest 下载放到 start.sh 运行时做。

Space 一直 Building 或 Starting

先看 Logs。只要日志里已经看到服务启动成功,可以直接访问 Space 域名测试。有时页面状态显示会滞后。

/cpm/ 打开后 404

通常是 CPA Manager Plus 重定向到了 /management.html,但 Caddy 把这个路径转给了 CPA。解决方式是给 /cpm/ 单独 rewrite:

1
2
3
4
handle /cpm/ {
rewrite * /management.html
reverse_proxy 127.0.0.1:18317
}

/ 返回 404

这不一定是错误。CPA 主服务本身可能没有根页面。更可靠的验证方式是:

1
2
3
/cpm/
/usage-service/info
/v1

API Key 或认证文件不见了

CPA 的数据库配置和认证文件不是一回事。数据库可以保存配置和用量,认证文件通常走 auth-dir。如果 auth-dir 放在 /tmp,Space 重启后认证文件可能丢失。

如果要长期稳定,建议:

1
2
配置和用量 -> PostgreSQL
认证文件 -> 重新上传,或改造为持久化路径

Codex 提示 Missing environment variable

说明当前 Codex 进程没有读到环境变量。设置后需要重开终端或重启 Codex:

1
[Environment]::SetEnvironmentVariable("CPA_SPACE_API_KEY", "your-cpa-api-key", "User")

Codex 提示 Approaching rate limits

这个提示通常和当前模型、上游账号额度、共享 API Key 的整体使用量有关,不代表一句话就把额度用完了。如果默认使用高消耗模型,比如 gpt-5.5,Codex 会更早提示切换到 mini。

总结

这套方案的核心是:Hugging Face Space 只暴露一个公网端口,所以要在单容器里跑多进程,再用 Caddy 做内部路由。

最终结构是:

1
2
3
4
5
6
7
Hugging Face HTTPS
|
v
Space :7860 Caddy
|
+-- / -> CLIProxyAPI :8317
+-- /cpm/ -> CPA Manager Plus :18317

现在的版本更新策略是:

1
2
3
4
容器启动
-> 尝试从 GitHub Releases 下载 latest CPA / CPA Manager Plus
-> 下载成功:使用最新版
-> 下载失败:使用镜像内置兜底二进制

这样日常部署不用频繁改版本号,同时也不会因为 GitHub Releases 临时访问失败导致整个 Space 起不来。缺点是免费 Space 的文件系统不适合保存重要认证文件,生产使用时要特别注意 Secret 管理、数据库持久化和访问密钥轮换。

评论




站点访问量 Loading… 站点访客数 Loading…