配套开源仓库:本文的一键部署版本,一条命令完成 Docker 部署、key 生成与配置渲染,另附客户端配置生成脚本与 Nginx 示例:

kuangyongjie/cliproxyapi-codex-deploy

git clone https://github.com/kuangyongjie/cliproxyapi-codex-deploy.git
cd cliproxyapi-codex-deploy
./scripts/setup.sh

如果你现在已经把 Codex 用顺手了,迟早会碰到一个挺现实的问题:我到底要不要继续老老实实用官方直登,还是干脆自己加一层代理,把 Codex 放进我自己的 AI 开发环境里。

这件事一开始看起来像“技术洁癖”,但真用起来就不是了。你如果只有一台电脑、一个账号,而且平时主要就是自己本地开工,那官方直接登录确实最轻,没什么好争的。装完能用,少一层服务,少一层维护,也少一层排障路径。这种场景下,为了“可控”强行多架一个代理,很多时候只是给自己加活。

但一旦你开始有别的想法,情况就变了。比如你不想每台机器都重新登一遍;比如你希望 Codex、Claude Code、Gemini CLI 这些东西后面都能走同一个入口;再比如你想把 OAuth 登录状态、访问控制、兼容接口、甚至后面的多账号轮询都收进一台你自己能控的服务里。到了这个阶段,像 CLIProxyAPI 这种项目就很有意思,因为它不是单纯“帮你转发请求”,而是在做一层可控的接入面。

我当时真开始认真看这条路,也不是因为“想折腾得更高级”,而是因为发现官方直登很适合单机开工,但一旦我想把 Codex 接进自己平时那套 VPS、自部署工具和脚本环境里,很多东西就开始变散了。登录状态在本机,别的项目各配各的入口,后面再加一个调用端,又是一轮重复配置。核心问题其实很简单:如果你想把 Codex 通过 CLIProxyAPI 反代出去,用 Docker 跑在一台 Linux 服务器上,跑通 OpenAI OAuth,然后回到本地 Codex App 改配置接过去,这条链路到底应该怎么定,哪里最容易翻车。

代理方案到底好在哪

我先把结论说清楚:如果你只图最快开工,官方直登还是最低摩擦方案;但如果你要的是“以后我的 Codex 不只是一个 app,而是我整套开发环境的一部分”,那代理方案就很值得。它最大的价值,不是炫,而是把“某台机器上的登录状态”变成“我自己掌控的一层基础设施”。

而且这个“基础设施”一旦搭出来,受益的通常不只是 Codex 自己。你很快就会发现,真正方便的地方在于你终于有了一个可以复用的 OpenAI-compatible 入口。以后你自己部署的其他项目,只要支持自定义 base_url 或 OpenAI 兼容接口,理论上都能直接接过来。像龙虾这类项目,其实就很适合吃这种统一入口。它本身作为前端或调用层,并不值得你再单独维护一套上游登录状态;让它和 Codex 一样都挂在这层代理后面,实际会省心很多。别的自部署前端、Agent 面板、脚本服务也一样,它们只要认你这层代理发出去的 api-key,就能共用同一个入口。

这里最容易被低估的一点,是“共享”的其实不是某个写死在项目里的上游密钥,而是代理层保存下来的那份 OAuth / token 状态。以前你可能要在 Codex、脚本、面板、另一个容器里各自处理登录,机器一换、容器一重建,又得重新补一遍。现在变成上游状态集中保存在 CLIProxyAPI 这一层,下游项目只认代理地址和代理 key。这个差别看起来像少填几项配置,实际上是把整套维护方式改了。你后来再接一个龙虾项目,或者再补一个只会读 OpenAI-compatible 接口的内部工具,脑子里想的就不是“这次又要怎么登录一次”,而是“给它发哪把 key、挂到哪个入口”。

这件事对独立开发者特别有价值,因为它把“每个项目各配一遍”变成了“上游只配一次,下游按项目分发自己的访问 key”。换句话说,真正敏感、真正麻烦的那层 OAuth 状态和 token,不再散落在每台机器、每个容器、每个项目配置里,而是被你集中收口到了代理服务这一层。以后你换机器、重装客户端、补一个新项目,改的通常只是下游访问方式,不是再从头重做上游授权。

而且这里还有个很实际的好处:代理 key 的策略可以按你的阶段来。你现在项目不多,完全可以先让几个自己控制的内部项目共用一把代理 key,先把链路打通,省掉重复配置。等后面 Codex、龙虾项目、聊天前端、自动化脚本这些东西越挂越多,再按项目拆 key、做限流、做日志区分。也就是说,这层代理既适合你现在先图省事,也给你后面规范化留了位置。

如果你后面还会继续接别的工具,这种好处会越来越明显。比如今天你先让 Codex 走这层代理,明天你再把 Claude Code、Gemini CLI,或者你自己部署的别的调用端也放进来,外部看到的依旧可以是一个统一入口。甚至从运维视角看,这也很省脑子:限流、访问控制、日志、上游切换、模型映射,都开始有了一个固定落点,而不是分散在每个项目自己的环境变量里。

先把链路拆成三块

我最后理解这条链路时,是把它硬拆成了三块。第一块是服务器上跑起来的 CLIProxyAPI 服务本身。第二块是 Codex 的 OAuth 登录状态,要真正写进这个服务挂载出来的认证目录里。第三块才是本地 Codex 客户端配置,也就是 ~/.codex/config.toml 和 ~/.codex/auth.json 这两个文件。只要你脑子里这三层不混,后面其实就顺了;反过来,很多人第一次接不通,往往就是把“代理登录”和“客户端配置”当成了一件事。

CLIProxyAPI 官方 README 现在已经明确写了它支持 Codex via OAuth,也给了 Docker、Docker Compose、Codex 客户端配置文档。光看官方文档,其实路线并不复杂。真正麻烦的是第一次做时,你很容易误判每个文件到底是给谁用的。config.yaml 是代理服务的,auth/ 是代理登录状态的,~/.codex/config.toml 是本地 Codex App 的,这三份东西别搅在一起。

服务器端:Docker 部署与 OAuth 登录

如果是我自己搭,我会先在服务器上收一个很小的目录,不去搞太花的结构。差不多这样就够了:

/opt/cliproxyapi/
├── docker-compose.yml
├── config.yaml
└── auth/

这里的 auth/ 很关键,因为官方 auth-dir 默认就是 ~/.cli-proxy-api。你用 Docker 时,本质上就是把容器里的这个目录映射出来,让 OAuth 登录后留下来的状态别随着容器消失。很多人第一次能登录成功,但第二次容器一重建就发现状态没了,问题基本都出在这里。

config.yaml 我不建议一上来写得很大。先让它稳稳跑起来再说。官方 config.example.yaml 里,最值得先关心的其实就这么几项:

host: ""
port: 8317
auth-dir: "~/.cli-proxy-api"

api-keys:
  - "<proxy-api-key>"

remote-management:
  allow-remote: false
  secret-key: ""

这里有两个点最好一开始就想明白。一个是 api-keys 不是 OpenAI 的 key,它是你下游客户端访问这层代理时要带的访问令牌。这个设计反而正是代理方案的价值之一,因为你可以给不同项目发不同的下游 key。比如 Codex App 用一个,龙虾这类自部署项目再用一个,后面想吊销、轮换或者拆分权限时,动的是代理层,不是每个项目都回头重配上游。另一个是 remote-management.allow-remote,如果你现在只是拿它服务 Codex,没有明确的远程管理需求,就先关着。这个项目默认也是关的,我觉得这个默认值挺对,因为很多人刚搭起来就喜欢顺手把控制面也一起暴露,后面反而给自己留坑。

Docker Compose 这块,我会先定成最保守也最容易解释的版本。核心目的只有两个:把 API 端口映出来,把认证目录和配置文件挂进去。

services:
  cli-proxy-api:
    image: eceasy/cli-proxy-api:latest
    container_name: cli-proxy-api
    restart: unless-stopped
    ports:
      - "127.0.0.1:8317:8317"
      - "127.0.0.1:1455:1455"
    volumes:
      - ./config.yaml:/CLIProxyAPI/config.yaml
      - ./auth:/root/.cli-proxy-api

这两个端口我都绑在了 127.0.0.1 上,是有意的。API 端口只需要被本机的 Nginx 访问到(后文 Nginx 本来就是反代 http://127.0.0.1:8317),而 OAuth 回调监听面曾经有过对外发布的历史风险(上游 issue #3463)。默认把这两个端口暴露到公网,等于把一个临时的登录面变成了永久的攻击面,所以不要把它当成安全的默认值。如果你的网络拓扑确实需要更广的访问,再单独声明限制条件。

再多说一句镜像 tag::latest 是浮动 tag。本文的 v8 字段对照核对的是上游仓库 2026-10-14 的 main 分支,并没有用这个镜像实际跑过。部署前先确认你拉到的 tag 还是这个布局,或者直接锁定一个自己验过的 digest。

这里把 1455 也映射出来,不是因为你的业务流量都走它,而是因为官方文档明确写了 Codex 的本地 OAuth 回调用这个端口。这个细节很容易被忽略,因为很多人眼里只盯着 API 端口 8317,结果服务明明跑着,--codex-login 也执行了,最后授权跳不回来,或者浏览器里转完发现代理这边没拿到状态。不是 CLIProxyAPI 坏了,是你把 OAuth 这一段链路想得太简单了。

等 Compose 起好以后,先别急着改本地 Codex。这个顺序真的很重要,应该先验证代理自己能不能拿到 Codex 的 OAuth 状态。官方给的命令是:

docker compose exec cli-proxy-api /CLIProxyAPI/CLIProxyAPI -no-browser --codex-login

我自己很喜欢这个 -no-browser,尤其是在远程 Linux 服务器上。它不会尝试在容器里开浏览器,而是把登录 URL 直接吐出来,你复制到本地浏览器里做授权就行。这一步你要理解成“给代理服务登录”,不是“给我的 Codex App 登录”。也就是说,真正被授权的是 CLIProxyAPI 这一层,授权成功后,状态应该会落到你挂载出来的 auth/ 目录里。

关于这个回调,有一件事在远程机器上特别容易搞错。浏览器跑在你的本地电脑上,所以 http://localhost:1455/auth/callback 解析到的是你的本地电脑,而不是 VPS——不管容器的端口映射怎么写。对于远程 Linux/VPS 场景,真正能闭环的顺序是:

  1. 先在本地(不是服务器上)建好隧道:
    ssh -N -L 1455:127.0.0.1:1455 <user>@<vps>
  2. 再执行上面的登录命令。CLI 会输出 OAuth URL(-no-browser 下它甚至会自己打印 SSH 隧道指引)。
  3. 在本地浏览器里打开这个 URL 完成授权,localhost:1455 的回调就会经由隧道回到容器。

如果跳过隧道,CLI 会停在 “Waiting for Codex authentication callback…”,约 15 秒后提示 Paste the Codex callback URL (or press Enter to keep waiting):,这时直接把浏览器地址栏里的完整回调 URL 粘贴进去就行——这是 CLI 自带的手动路径,不是野路子。

如果这一段没跑通,后面你本地怎么改 config.toml 都没意义。因为客户端只是换了入口,真正能不能代表你去拿 Codex 结果,还是得看代理自己有没有那份 OAuth 状态。所以第一次做时,我会先看两个东西:一个是 docker compose logs -f 的实时输出,另一个是 auth/ 目录里有没有新文件写进去。前者告诉你登录流程卡在哪,后者告诉你这次授权到底是不是落盘了。

config.yaml 字段速查(v8)

前面示例里的 config.yaml 用的是旧版顶层字段写法(host、port、auth-dir、api-keys、remote-management),照样能用。但上游已经切到 v8 布局(config-version: 8),所有配置都收进具名小节里。如果你对照的是上游仓库现在的 config.example.yaml,看到的就是新写法。下面是 Codex 代理场景实际用到的字段对照,每个字段名都已对照上游 main 分支(2026-10-14 实取)核对过。Plus fork 可能有分化,套用前先确认它们锁定的镜像版本和 config.example.yaml。

字段(v8)旧写法说明
config-version: 8—声明 v8 布局。新旧写法同时出现时,v8 优先。
server.hosthost绑定网卡。空字符串表示监听所有网卡。
server.portport客户端调用的 API 端口,本文用 8317。
oauth.auth-dirauth-dirCodex OAuth 状态存放位置,必须和 auth/ 的挂载对应上,否则重启就丢登录。
access.api-keysapi-keys发给下游客户端的 key,不是上游 OpenAI 凭据。
management.allow-remoteremote-management.allow-remote没有明确的远程管理需求就保持 false。
management.secret-keyremote-management.secret-key管理 API 的密钥,留空则管理 API 直接禁用。
client.codex.optimize-multi-agent-v2—可选,Codex Desktop / codex-tui 多 agent 场景优化,默认 false。

按本文配置写成的 v8 风格最小配置:

config-version: 8

server:
  host: ""
  port: 8317

oauth:
  auth-dir: "~/.cli-proxy-api"

access:
  api-keys:
    - "<proxy-api-key>"

management:
  allow-remote: false
  secret-key: ""

两点值得单独说。第一,access.api-keys 和旧写法的 api-keys 是同一份名单:发给下游的 key,永远不是上游密钥。第二,容器里 oauth.auth-dir 默认解析到 /root/.cli-proxy-api,这正是 compose 里把 ./auth 挂到那里的原因。两边改了一边没改,“昨天还好好的今天重启就掉登录”的坑就回来了。

Docker Compose 独立路径

文首的配套仓库把下面这些做成了一键脚本。如果你想每个文件都自己掌控、看清楚每一步在跑什么,下面是同一套配置的独立 compose 版本,不依赖任何辅助脚本。

目录还是前面那个小结构:

/opt/cliproxyapi/
├── docker-compose.yml
├── config.yaml
└── auth/

compose 文件就用“服务器端”上面那一份——同样的 127.0.0.1 绑定、同样的挂载,按上游仓库自己的 docker-compose.yml 裁到本文需要的样子。这里不再重复贴第二份,因为正确配置应该只有一份源头,照着上面复制即可。./auth 还是要落到 oauth.auth-dir 指的地方,否则每次重建容器 OAuth 状态就丢。

启动并观察:

docker compose config
docker compose up -d
docker compose logs -f

第一条值得养成习惯:docker compose config 只渲染生效配置、不启动任何容器,缩进写错、挂载路径写错这类问题在变成运行时谜题之前就能发现。

然后按上一节同样的方式登录:

docker compose exec cli-proxy-api /CLIProxyAPI/CLIProxyAPI -no-browser --codex-login

说句实话:上面 compose 文件的每个字段都对照过上游仓库的 docker-compose.yml 和本文配置,但这次更新我没有在真机上实际执行过这份文件。如果你的环境不一样(比如 rootless Docker、非默认数据目录),卷路径是最先要核对的地方。

本地端:Codex 客户端与 Nginx 配置

到这里很多人会有个误区:既然代理已经做完 OpenAI OAuth 了,那本地 Codex 不是就可以什么都不用配了吗?实际上不是。按 CLIProxyAPI 官方 Codex 客户端文档,本地还是要改两个文件:~/.codex/config.toml 和 ~/.codex/auth.json。只是这里的逻辑跟你直连 OpenAI 时已经完全不一样了。

你可以把它理解成两层 key。上游那层,是代理自己持有的 OAuth/token 状态,它代表的是“CLIProxyAPI 这层能不能替你去拿 Codex 能力”;下游这层,则是你发给客户端或别的项目的访问 key,它代表的是“谁可以调用我的代理”。这个拆分很值钱,因为以后你再接一个新项目,通常不需要再碰上游授权,只需要给它一把新的下游 key,再把 base_url 指过来就行。

如果你的代理跑在远程服务器,我会直接把 config.toml 理解成“把 Codex 的 provider 指到我自己的域名上”。像这样:

model_provider = "cliproxyapi"
model = "gpt-5-codex"
model_reasoning_effort = "high"

[model_providers.cliproxyapi]
name = "cliproxyapi"
base_url = "https://codex-proxy.example.com/v1"
wire_api = "responses"

这里最容易写错的,其实反而不是模型名,而是两处小地方。第一处是 base_url,很多人明明服务跑在远程域名上了,结果还沿用官方示例里的 http://127.0.0.1:8317/v1,最后本地 Codex 当然还是在找自己机器上的代理。第二处是 wire_api = "responses",这个不能漏。CLIProxyAPI 官方给 Codex 的兼容方式就是走 responses,少了这一句,客户端和代理经常会出现你看不懂的行为偏差。

至于 auth.json,它反而比很多人以为的简单得多。官方文档给的例子就是:

{
  "OPENAI_API_KEY": "sk-dummy"
}

第一次看到这里,很多人会皱眉头:我都走代理了,怎么还要填 OPENAI_API_KEY?其实你要把它理解成客户端的一个占位要求,而不是“这里真要放一个能用的 OpenAI key”。真正起作用的是上面的 config.toml provider 配置,这里的 sk-dummy 只是让 Codex 本地侧的配置结构完整。也正因为这样,我更不建议在这里手痒放进真的上游 key,你本来都已经把接入控制收口到代理层了,没必要又把敏感信息往客户端这一层塞回去。

如果你还想把 8317 收到一个正式域名后面,比如 codex-proxy.example.com,那 Nginx 也别搞太复杂。最重要的是保住 /v1/ 这个路径,不要为了看起来整齐把它改来改去。一个够用的写法就是:

server {
  listen 443 ssl http2;
  server_name codex-proxy.example.com;

  location /v1/ {
    proxy_pass http://127.0.0.1:8317/v1/;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}

只要这层入口稳定下来,它的意义就不只是给 Codex 用。很多自部署项目本身吃的就是 OpenAI-compatible 协议,它们往往也支持改 OPENAI_BASE_URL、OPENAI_API_BASE 或类似变量。到那一步,你其实已经不再是在“给 Codex 做个专用代理”,而是在给自己手上的一批项目搭一个统一 AI 网关。你今天先接 Codex,明天再接龙虾项目或别的前端,配置思路会非常像:

OPENAI_BASE_URL=https://codex-proxy.example.com/v1
OPENAI_API_KEY=<proxy-api-key-for-this-project>

真正舒服的地方就在这里。上游 OAuth、模型接入、账号轮询这些重东西,集中在 CLIProxyAPI 层;下游每个项目只管认一个稳定入口和一把自己的访问 key。以后你想换上游、加账号、做容灾、甚至把某个项目单独禁掉,都不用把所有客户端和所有自部署服务重新翻一遍。

如果你手上已经有别的自部署项目,这种统一入口的价值会更直接。比如你拿 Codex 做代码工作流,拿龙虾这种前端项目做日常对话或模型试验,再加几个内部脚本去跑自动化任务。以前这三类东西很容易各自带一套配置,哪个项目里到底塞了什么 key,过两周自己都记不清。现在它们都挂到同一个代理后面,真正长期保存的只有代理层那份上游状态,你要管理的是“哪个项目能不能用、用哪把代理 key、走哪个域名入口”,而不是到处找散落的 token。

最常见的翻车点

如果你问我实际最常见的翻车点是什么,我会说不是 Docker,也不是 Compose 本身,而是下面这些很细的小地方:

  • 代理服务已经跑起来了,但 auth/ 根本没拿到 Codex OAuth 状态
  • 本地 config.toml 改了,可 base_url 还写着 localhost
  • Nginx 只反代了域名,没有把 /v1/ 路径保留下来
  • 忘了 wire_api = "responses"
  • 以为 auth.json 要填真 OpenAI key,又把本来不需要暴露的东西放回本地

你会发现,这些问题都不是“命令写错”,而是你对这条链路的职责分层有没有想清楚。直接登录当然简单,因为认证、请求、客户端都在一层里;CLIProxyAPI 这条路则是把登录状态、代理服务、反代入口、本地客户端拆开了。拆开以后更灵活,但第一遍接的时候脑子里一定要有拓扑。

所以最后还是回到那个问题:它跟直接登录到底有什么区别,值不值得。我的看法挺明确的。直接登录适合“我自己马上要用”;CLIProxyAPI 更适合“我要把 Codex 变成我整套 AI 开发环境里可控的一部分”。前者低摩擦,后者高自由度。你如果已经开始同时用多种 AI coding 工具,或者有多台机器要保持一致入口,那后者的价值会越来越明显。尤其当你手里不只 Codex 一个调用端,而是还挂着龙虾这类自部署服务时,这个差别会一下子变得很具体:你终于不用每个项目都重新处理一次上游认证,也不用把密钥和 token 分散埋在不同容器和配置文件里。

还有一件事我觉得很多人会后知后觉:既然你已经开始把这类东西做成自己的基础设施,就别把 config.yaml 和 auth/ 当成随时可重来的临时文件。它们其实已经是状态了。真要长期跑,最好顺手把这两块也纳进备份里。不然哪天容器重建、机器迁移、目录误删,你会发现最费时间的从来不是重拉镜像,而是把那些登录状态和细碎配置重新拼回来。

如果你现在就在搭 Codex 工作流,这篇 新版 Codex 有什么变化:为什么它现在更像一个开发环境 可以顺手一起看;如果你准备把这个代理长期挂在 VPS 上,那篇 Docker 数据卷自动备份到 GitHub:适合 1Panel 的低成本容灾方案 也值得补一下,因为像认证目录、配置文件这种东西,真出过一次问题就知道备份不是锦上添花。

Runbook:CLIProxyAPI 接不上 Codex 时

这条链路出问题时,按职责分层排比“从头重装”更快:

  1. Proxy 服务:先确认 CLIProxyAPI 容器本身健康,8317 的 API 服务在监听。
  2. OAuth 状态:执行官方 Codex 登录流程后,确认挂载出来的 auth/ 目录真的产生了状态文件。
  3. 客户端入口:本地 ~/.codex/config.toml 的 base_url 必须指向真实远程域名,而不是继续留在 localhost。
  4. 协议:确认 provider 仍然使用 wire_api = "responses"。
  5. 反代:Nginx 要完整保留 /v1/ 路径,再去看上游返回;不要把路径改写问题误判成模型问题。
  6. 下游 key:auth.json 只满足客户端结构;真正的访问控制应该由代理层的 key 管理,不要重新把上游真实凭据散回客户端。

如果你是从官方 localhost 示例迁到远程域名,下面这个 diff 比重新对照整段配置更容易发现错误:

-base_url = "http://127.0.0.1:8317/v1"
+base_url = "https://codex-proxy.example.com/v1"

重新做 OAuth 时,真正要执行的仍然是代理容器里的登录命令:

$ docker compose exec cli-proxy-api /CLIProxyAPI/CLIProxyAPI -no-browser --codex-login