使用 Docker Compose 部署 cpa-manager-plus 完整体:集成 CLI Proxy API、Web 监控与 Nginx 统一网关

使用 Docker Compose 部署 cpa-manager-plus 完整体:集成 CLI Proxy API、Web 监控与 Nginx 统一网关

  1. 我的工具箱 🪜
  2. 2026-07-10 12:30
  3. 12 min read

平时折腾各类命令行 AI 工具(比如 Claude Code、Gemini CLI、Cursor 或各类开源 Agent)时,很多人都会用 CLI Proxy API (CPA) 来统一管理账号池、做渠道路由和格式转换。

为了更直观地看调用统计、管理 API Key 和监控模型消耗,社区又衍生出了配套的 Web 可视化面板 cpa-manager-plus

但如果按照常规方式分别部署这两个服务,通常会遇到几个烦心的问题:

  • cli-proxy-api 默认跑在 8317 端口,cpa-manager-plus 默认跑在 18317 端口;
  • 客户端请求走一个端口,网页后台看数据又走另一个端口,防火墙得多开端口,有些环境下还会遇到跨域或回调 URL 不一致的问题;
  • 两个服务各自需要种子配置文件,如果在宿主机单独挂载空文件,容易踩到 Docker 默认把不存在的路径当成目录创建的经典天坑。

为了让整套服务像一个独立的“中台系统”一样开箱即用,我参考CPAMP官方文档选用 Docker Compose 配上 Nginx 反代方案,使用Full Mode把它们打包成了这套***“完整体”***:对外只暴露一个统一的 80 端口(也可挂载域名与 SSL),所有路由自动分流,配置内聚,单文件一键启停。


整体架构与分流逻辑

这套“完整体”部署在自建VPS的Coolify面板中,包含三个容器服务,彼此通过 Docker 内部网络通信:

  1. gateway(Nginx):统一入口(80 端口)。负责智能分流、关闭缓冲(确保 SSE 流式打字机效果)以及 WebSocket 长连接支持;
  2. cpa-manager-plus:负责 Web 管理后台、Token 与用量统计(SQLite)、健康检查接口;
  3. cli-proxy-api:负责大模型核心 API 代理中转、鉴权与模型路由。

流量路径如下:

                              ┌────────────────────────────────────────┐
                              │          Nginx 统一网关 (:80)          │
                              └──────────────────┬─────────────────────┘

                        ┌────────────────────────┴────────────────────────┐
                        │ 路径分流                                        │
                        ▼                                                 ▼
        [/management.html, /health, /setup,              [/v1/, /v1beta/, /backend-api/,
         /usage-service/, /v0/management/, ...]           /anthropic/callback, /api/, ...]
                        │                                                 │
                        ▼                                                 ▼
       ┌─────────────────────────────────┐               ┌─────────────────────────────────┐
       │   cpa-manager-plus (:18317)     │               │      cli-proxy-api (:8317)      │
       │   (Web 管理面板 / 用量统计 SQLite) │               │      (大模型代理 / 路由 / 插件)   │
       └─────────────────────────────────┘               └─────────────────────────────────┘

部署准备:配置环境变量

在放 docker-compose.yml 的同一目录下,先新建一个 .env 环境变量文件,填入你的密钥信息:

# cpa-manager-plus 的管理后台访问密钥(必填,请设置长随机串)
CPA_MANAGER_ADMIN_KEY="your-long-random-admin-key-here"

# cli-proxy-api 的远程管理通信密钥(必填,保持一致)
CPA_MANAGEMENT_KEY="your-long-random-management-key-here"

# 客户端调用 API 时的默认 Key(可选,多个可用 JSON 数组格式)
CPA_API_KEY='["sk-custom-cpa-key-001"]'

提示:Compose 文件中使用了 ${VAR:?error_msg} 语法,如果未设置这些核心环境变量,Docker Compose 会直接报错阻止启动,防止空密码裸奔。

在Docker Deploy后,可以在General - Service Stack 中看到Services 列表,包含gateway/cli-proxy-api/cpa-manager-plus这3个服务,如果要绑定域名,就把解析执行VPS IP的域名配置在gateway服务的Settings中。


完整 Docker Compose 配置(V2.0 推荐版)

这个版本充分利用了 Docker Compose 的 configs 特性与 YAML 块标量符(|),不仅排版清晰,而且不需要在宿主机手动创建初始配置文件,Docker 会在启动时直接将内容注入容器。

创建 docker-compose.yml

services:
  cli-proxy-api:
    image: 'eceasy/cli-proxy-api:latest'
    restart: unless-stopped
    command:
      - sh
      - '-c'
      - |
        mkdir -p /app/data/auths /app/data/plugins
        if [ ! -s /app/data/config.yaml ]; then
          cp /seed/config.yaml /app/data/config.yaml
        fi
        chmod 600 /app/data/config.yaml
        exec ./CLIProxyAPI -config /app/data/config.yaml
    expose:
      - '6006:8317'
    volumes:
      - 'cpa-data:/app/data'
    configs:
      - source: cpa-config-seed
        target: /seed/config.yaml

  cpa-manager-plus:
    image: 'seakee/cpa-manager-plus:latest'
    restart: unless-stopped
    expose:
      - '6015:8317'
    environment:
      HTTP_ADDR: '0.0.0.0:18317'
      USAGE_DB_PATH: /data/usage.sqlite
      CPA_MANAGER_DATA_KEY_PATH: /data/data.key
      CPA_MANAGER_ADMIN_KEY: '${CPA_MANAGER_ADMIN_KEY:?set-a-long-random-admin-key}'
      USAGE_COLLECTOR_MODE: auto
      USAGE_BATCH_SIZE: '100'
      USAGE_POLL_INTERVAL_MS: '500'
      USAGE_QUERY_LIMIT: '50000'
    volumes:
      - 'cpa-manager-plus-data:/data'
    depends_on:
      cli-proxy-api:
        condition: service_started
    healthcheck:
      test:
        - CMD
        - wget
        - '-qO-'
        - 'http://127.0.0.1:18317/health'
      interval: 10s
      timeout: 3s
      retries: 3

  gateway:
    image: 'nginx:alpine'
    restart: unless-stopped
    ports:
      - '80:80'
    configs:
      - source: cpa-router
        target: /etc/nginx/conf.d/default.conf
    depends_on:
      cpa-manager-plus:
        condition: service_healthy
      cli-proxy-api:
        condition: service_started
    healthcheck:
      test:
        - CMD-SHELL
        - 'wget -qO- http://127.0.0.1/health >/dev/null || exit 1'
      interval: 10s
      timeout: 3s
      retries: 3

configs:
  cpa-config-seed:
    content: |
      host: "0.0.0.0"
      port: 8317

      remote-management:
        allow-remote: true
        secret-key: "${CPA_MANAGEMENT_KEY:?set-a-long-cpa-management-key}"
        disable-control-panel: false

      auth-dir: "/app/data/auths"

      api-keys: ${CPA_API_KEY:-["sk-default-key"]}

      debug: false
      logging-to-file: true
      logs-max-total-size-mb: 100
      usage-statistics-enabled: true
      redis-usage-queue-retention-seconds: 60
      proxy-url: ""
      request-retry: 3
      max-retry-interval: 30

      plugins:
        enabled: true
        dir: "/app/data/plugins"

      routing:
        strategy: "round-robin"

  cpa-router:
    content: |
      map $$http_upgrade $$connection_upgrade {
          default upgrade;
          '' close;
      }

      upstream cpa_api {
          server cli-proxy-api:8317;
      }

      upstream cpamp {
          server cpa-manager-plus:18317;
      }

      server {
          listen 80;
          server_name _;

          client_max_body_size 64m;
          proxy_http_version 1.1;
          proxy_read_timeout 3600s;
          proxy_send_timeout 3600s;
          proxy_buffering off;

          proxy_set_header Host $$host;
          proxy_set_header X-Real-IP $$remote_addr;
          proxy_set_header X-Forwarded-For $$proxy_add_x_forwarded_for;
          proxy_set_header X-Forwarded-Proto $$http_x_forwarded_proto;
          proxy_set_header Upgrade $$http_upgrade;
          proxy_set_header Connection $$connection_upgrade;

          # 根路径重定向到管理界面
          location = / { return 302 /management.html; }

          # 管理后台相关路由转发给 cpa-manager-plus
          location = /management.html { proxy_pass http://cpamp; }
          location = /health { proxy_pass http://cpamp; }
          location = /status { proxy_pass http://cpamp; }
          location = /setup { proxy_pass http://cpamp; }
          location ^~ /usage-service/ { proxy_pass http://cpamp; }
          location ^~ /v0/management/ { proxy_pass http://cpamp; }
          location ^~ /v0/resource/plugins/ { proxy_pass http://cpamp; }
          location = /models { proxy_pass http://cpamp; }

          # API 与模型调用转发给 cli-proxy-api
          location ^~ /v1/ { proxy_pass http://cpa_api; }
          location ^~ /v1beta/ { proxy_pass http://cpa_api; }
          location ^~ /backend-api/codex/ { proxy_pass http://cpa_api; }
          location ^~ /api/ { proxy_pass http://cpa_api; }

          # 各厂商 OAuth 与专用回调
          location = /v1internal:method { proxy_pass http://cpa_api; }
          location = /healthz { proxy_pass http://cpa_api; }
          location = /anthropic/callback { proxy_pass http://cpa_api; }
          location = /codex/callback { proxy_pass http://cpa_api; }
          location = /google/callback { proxy_pass http://cpa_api; }
          location = /antigravity/callback { proxy_pass http://cpa_api; }

          # 默认兜底走 API 代理
          location / { proxy_pass http://cpa_api; }
      }

volumes:
  cpa-data:
  cpa-manager-plus-data:

Coolify展示版本:转义字符单行版

在Coolify Resource中保存完整的 YAML配置时,会把配置强行压缩成一行展示,用 \n 进行转义。排版密密麻麻,一旦改动一个端口或路径很容易看花眼(Format保存也不行)。

这里把转义写法保留下来作为备忘对比:

点击展开查看Coolify版本(使用转义字符的呈现写法)
services:
  cli-proxy-api:
    image: 'eceasy/cli-proxy-api:latest'
    restart: unless-stopped
    command:
      - sh
      - '-c'
      - "mkdir -p /app/data/auths /app/data/plugins\nif [ ! -s /app/data/config.yaml ]; then\n  cp /seed/config.yaml /app/data/config.yaml\nfi\nchmod 600 /app/data/config.yaml\nexec ./CLIProxyAPI -config /app/data/config.yaml\n"
    expose:
      - '6006:8317'
    volumes:
      - 'cpa-data:/app/data'
    configs:
      - source: cpa-config-seed
        target: /seed/config.yaml

  cpa-manager-plus:
    image: 'seakee/cpa-manager-plus:latest'
    restart: unless-stopped
    expose:
      - '6015:8317'
    environment:
      HTTP_ADDR: '0.0.0.0:18317'
      USAGE_DB_PATH: /data/usage.sqlite
      CPA_MANAGER_DATA_KEY_PATH: /data/data.key
      CPA_MANAGER_ADMIN_KEY: '${CPA_MANAGER_ADMIN_KEY:?set-a-long-random-admin-key}'
      USAGE_COLLECTOR_MODE: auto
      USAGE_BATCH_SIZE: '100'
      USAGE_POLL_INTERVAL_MS: '500'
      USAGE_QUERY_LIMIT: '50000'
    volumes:
      - 'cpa-manager-plus-data:/data'
    depends_on:
      cli-proxy-api:
        condition: service_started
    healthcheck:
      test:
        - CMD
        - wget
        - '-qO-'
        - 'http://127.0.0.1:18317/health'
      interval: 10s
      timeout: 3s
      retries: 3

  gateway:
    image: 'nginx:alpine'
    restart: unless-stopped
    ports:
      - '80:80'
    configs:
      - source: cpa-router
        target: /etc/nginx/conf.d/default.conf
    depends_on:
      cpa-manager-plus:
        condition: service_healthy
      cli-proxy-api:
        condition: service_started
    healthcheck:
      test:
        - CMD-SHELL
        - 'wget -qO- http://127.0.0.1/health >/dev/null || exit 1'
      interval: 10s
      timeout: 3s
      retries: 3

configs:
  cpa-config-seed:
    content: "host: \"0.0.0.0\"\nport: 8317\n\nremote-management:\n  allow-remote: true\n  secret-key: \"${CPA_MANAGEMENT_KEY:?set-a-long-cpa-management-key}\"\n  disable-control-panel: false\n\nauth-dir: \"/app/data/auths\"\n\napi-keys:\n  - \"${CPA_API_KEY:?set-a-cpa-client-api-key}\"\n\ndebug: false\nlogging-to-file: true\nlogs-max-total-size-mb: 100\nusage-statistics-enabled: true\nredis-usage-queue-retention-seconds: 60\nproxy-url: \"\"\nrequest-retry: 3\nmax-retry-interval: 30\n\nplugins:\n  enabled: true\n  dir: \"/app/data/plugins\"\n\nrouting:\n  strategy: \"round-robin\"\n"
  cpa-router:
    content: "map $$http_upgrade $$connection_upgrade {\n    default upgrade;\n    '' close;\n}\n\nupstream cpa_api {\n    server cli-proxy-api:8317;\n}\n\nupstream cpamp {\n    server cpa-manager-plus:18317;\n}\n\nserver {\n    listen 80;\n    server_name _;\n\n    client_max_body_size 64m;\n    proxy_http_version 1.1;\n    proxy_read_timeout 3600s;\n    proxy_send_timeout 3600s;\n    proxy_buffering off;\n\n    proxy_set_header Host $$host;\n    proxy_set_header X-Real-IP $$remote_addr;\n    proxy_set_header X-Forwarded-For $$proxy_add_x_forwarded_for;\n    proxy_set_header X-Forwarded-Proto $$http_x_forwarded_proto;\n    proxy_set_header Upgrade $$http_upgrade;\n    proxy_set_header Connection $$connection_upgrade;\n\n    location = / { return 302 /management.html; }\n\n    location = /management.html { proxy_pass http://cpamp; }\n    location = /health { proxy_pass http://cpamp; }\n    location = /status { proxy_pass http://cpamp; }\n    location = /setup { proxy_pass http://cpamp; }\n    location ^~ /usage-service/ { proxy_pass http://cpamp; }\n    location ^~ /v0/management/ { proxy_pass http://cpamp; }\n    location ^~ /v0/resource/plugins/ { proxy_pass http://cpamp; }\n    location = /models { proxy_pass http://cpamp; }\n\n    location ^~ /v1/ { proxy_pass http://cpa_api; }\n    location ^~ /v1beta/ { proxy_pass http://cpa_api; }\n    location ^~ /backend-api/codex/ { proxy_pass http://cpa_api; }\n    location ^~ /api/ { proxy_pass http://cpa_api; }\n\n    location = /v1internal:method { proxy_pass http://cpa_api; }\n    location = /healthz { proxy_pass http://cpa_api; }\n    location = /anthropic/callback { proxy_pass http://cpa_api; }\n    location = /codex/callback { proxy_pass http://cpa_api; }\n    location = /google/callback { proxy_pass http://cpa_api; }\n    location = /antigravity/callback { proxy_pass http://cpa_api; }\n\n    location / { proxy_pass http://cpa_api; }\n}\n"

volumes:
  cpa-data:
  cpa-manager-plus-data:

启动与运行验证

在Coolify项目Resource中可以直接通过Pull latest images & Restart 一键拉取镜像更新部署并重新启动。如果是使用自己安装的Docker Compose来执行,可以参考以下命令。

在配置目录执行启动:

docker compose up -d

启动之后,检查容器健康状态:

docker compose ps

可以看到由于配置了健康检查依赖链(gateway 等待 cpa-manager-plus 变为 healthy,后者等待 cli-proxy-api 启动完成),三个服务会按部就班全部就绪。

浏览器直接访问:

http://<你的服务器IP>/

页面会自动 302 跳转到 http://<你的服务器IP>/management.html。输入你在 .env 中配置的 CPA_MANAGER_ADMIN_KEY,就可以直接进入控制面板,开始添加上游 Token 和渠道了。


客户端接入方式

因为有 Nginx 统一网关在前面做路由转发,所有对大模型的请求直接走标准 80 端口(或者后面挂了 HTTPS 域名的 443 端口):

  • OpenAI 兼容接口 Base URLhttp://<服务器IP>/v1
  • Gemini 原生接口http://<服务器IP>/v1beta
  • API Key:填你在面板中生成的 Key(或 .env 里的初始 Key)

以常见的终端配置为例:

export OPENAI_BASE_URL="http://192.168.1.100/v1"
export OPENAI_API_KEY="sk-custom-cpa-key-001"

一般我是直接使用OpenAI兼容接口格式进行配置,常规Agent或者Chat App使用都支持。


几个关键细节与踩坑排查

  1. 为什么 Nginx 必须配置 proxy_buffering off
    大模型交互基本都是基于 SSE(Server-Sent Events)的流式打字机输出。如果 Nginx 默认开着缓冲区,Nginx 会试图把后端返回的数据攒够一个 Buffer 再发给客户端,导致前端感知就是卡顿几秒钟突然蹦出一大段字。关掉 proxy_buffering 才能做到真正的打字机逐字平滑输出。
  2. 种子配置初始化机制
    cli-proxy-api 启动命令中有一句检查:
    if [ ! -s /app/data/config.yaml ]; then cp /seed/config.yaml /app/data/config.yaml; fi
    这样既能保证首次启动时自动用 seed 生成默认配置,又不会在后续重启容器时覆盖掉你在管理面板里动态修改并保存的新配置。
  3. 数据持久化
    所有认证信息(Token、OAuth 凭据)保存在 cpa-data 命名卷,面板的调用统计数据保存在 cpa-manager-plus-data 的 SQLite 数据库中。后续需要迁移机子时,直接打包这两个 Docker 卷即可完成无缝平移。
DockerDocker ComposeCLI-Proxy-APICPA-Manager-PlusNginx折腾记录