
Codex2API Docker 使用宿主机代理OAuth Token 兑换 403 问题排查与解决方案1. 问题描述在 Linux 服务器上通过 Docker Compose 部署 Codex2API宿主机已经配置本地代理export http_proxyhttp://127.0.0.1:7890 export https_proxyhttp://127.0.0.1:7890 export all_proxysocks5://127.0.0.1:7891Codex2API 的.env中同时配置CODEX_PROXY_URLhttp://host.docker.internal:7890进入容器检查环境变量docker exec codex2api sh -c echo $CODEX_PROXY_URL可以正常得到http://host.docker.internal:7890说明.env已经通过 Docker Compose 正确注入容器。但是在 Codex2API 管理后台进行 OpenAI OAuth 授权时浏览器授权过程能够正常完成而在授权码兑换 Token 阶段失败授权码兑换失败: token 兑换失败 (HTTP 403)Codex2API 日志表现为POST /api/admin/oauth/generate-auth-url 200 POST /api/admin/oauth/exchange-code 502实际流程为浏览器 OAuth 授权成功 ↓ 获得 authorization code ↓ Codex2API 请求 OpenAI /oauth/token ↓ 上游返回 HTTP 403 ↓ Codex2API 将上游错误包装为 HTTP 502 ↓ 管理后台显示 Token 兑换失败因此需要判断问题究竟发生在宿主机代理 Docker 网络 Codex2API 代理配置 OpenAI OAuth Endpoint中的哪一层。2. 检查宿主机代理首先验证宿主机本地代理是否正常。执行curl -x http://127.0.0.1:7890 https://api.ipify.org如果能够正常返回代理出口公网 IP例如xxx.xxx.xxx.xxx说明宿主机 ↓ 127.0.0.1:7890 ↓ 代理节点 ↓ Internet链路正常。进一步测试 OpenAI OAuth Token Endpointcurl -x http://127.0.0.1:7890 \ -X POST https://auth.openai.com/oauth/token \ -H Content-Type: application/x-www-form-urlencoded \ --data grant_typeauthorization_codecodeinvalidclient_idinvalidredirect_urihttp://localhost此处故意提供错误的code和client_id。如果能够收到 OpenAI 返回的标准 JSON 错误例如 HTTP401而不是连接超时、拒绝连接或代理错误则说明127.0.0.1:7890 ↓ 代理节点 ↓ auth.openai.com ↓ /oauth/token网络本身是可达的。因此问题不在宿主机代理也不是 OpenAI/oauth/token完全不可访问。3. Docker 网络结构查看 Codex2API 使用的 Docker 网络docker network inspect codex2api_codex2api-net当前实际网络为Network: codex2api_codex2api-net Driver: bridge Subnet: 172.28.0.0/16 Gateway: 172.28.0.1 Redis: 172.28.0.2 PostgreSQL: 172.28.0.3 Codex2API: 172.28.0.4其中172.28.0.1是宿主机在codex2api-net这张 Docker Bridge 网络中的网关地址。网络结构可以理解为Linux 宿主机 │ │ 172.28.0.1 │ Docker Bridge 172.28.0.0/16 │ ├── Redis 172.28.0.2 ├── PostgreSQL 172.28.0.3 └── Codex2API 172.28.0.4需要特别注意宿主机中的 127.0.0.1与Docker 容器中的 127.0.0.1不是同一个网络空间。在 Codex2API 容器内部访问127.0.0.1:7890实际上是在访问Codex2API 容器自身的 7890而不是宿主机的代理。因此 Codex2API 无法直接使用http://127.0.0.1:7890作为宿主机代理地址。4. 配置host.docker.internal为了让 Codex2API 容器能够找到宿主机需要在docker-compose.yml中增加services: codex2api: extra_hosts: - host.docker.internal:172.28.0.1这一配置相当于在 Codex2API 容器内部/etc/hosts中加入172.28.0.1 host.docker.internal因此host.docker.internal ↓ 172.28.0.1验证docker exec codex2api getent hosts host.docker.internal正常应返回172.28.0.1 host.docker.internal此时 Codex2API 容器已经能够通过host.docker.internal找到服务器宿主机。5. 为什么还需要 socat虽然 Codex2API 已经能够访问宿主机的172.28.0.1但是宿主机真实代理只监听127.0.0.1:7890检查ss -lntp | grep :7890原始状态类似LISTEN ... 127.0.0.1:7890也就是说127.0.0.1:7890只有宿主机自身能够访问。Docker 容器尝试访问172.28.0.1:7890时并没有服务监听因此仍然无法使用代理。所以需要使用socat建立一层 TCP 转发172.28.0.1:7890 ↓ socat ↓ 127.0.0.1:7890这样 Docker 容器访问宿主机 Docker 网关的 7890就会被转发到真正的本地代理。6. socat 前台测试安装sudo apt install socat前台启动sudo socat \ TCP-LISTEN:7890,bind172.28.0.1,reuseaddr,fork \ TCP:127.0.0.1:7890参数含义TCP-LISTEN:7890监听 TCP 7890 端口。bind172.28.0.1只监听宿主机在 Codex2API Docker 网络中的地址。reuseaddr允许端口快速重新绑定。fork每个客户端连接创建独立子进程可以处理多个并发连接。TCP:127.0.0.1:7890将接收到的连接转发到宿主机真正的代理。启动后检查ss -lntp | grep :7890应同时看到127.0.0.1:7890 172.28.0.1:7890其中127.0.0.1:7890 → 原始宿主机代理 172.28.0.1:7890 → socat 为 Docker 提供的代理入口7. socat 后台运行进入 Codex2API 项目目录cd ~/work/ljj-work/codex2api后台运行sudo sh -c nohup socat TCP-LISTEN:7890,bind172.28.0.1,reuseaddr,fork TCP:127.0.0.1:7890 /dev/null ./codex2api-socat.log 21 其中nohup表示 SSH 会话退出后进程继续运行。/dev/null禁止后台进程继续读取当前终端避免出现Stopped (tty output) ./codex2api-socat.log 21将标准输出和错误输出统一写入./codex2api-socat.log表示后台运行。检查进程ps -ef | grep [s]ocat正常类似root 3186222 1 ... socat TCP-LISTEN:7890,bind172.28.0.1,reuseaddr,fork TCP:127.0.0.1:7890如果 PPID 为1说明该进程已经脱离当前 SSH 终端。8. 查看和管理 socat查看进程ps -ef | grep [s]ocat查看端口ss -lntp | grep :7890正常127.0.0.1:7890 172.28.0.1:7890查看日志cat ./codex2api-socat.log实时查看日志tail -f ./codex2api-socat.log停止 socatsudo pkill -f socat TCP-LISTEN:7890,bind172.28.0.1停止后ss -lntp | grep :7890正常只剩127.0.0.1:78909. 测试 Docker 到宿主机代理首先测试 TCP 连通性docker exec codex2api sh -c \ nc -z -w 3 host.docker.internal 7890; echo exit$?如果返回exit0说明Codex2API ↓ host.docker.internal ↓ 172.28.0.1:7890已经连通。进一步测试 HTTP CONNECTdocker exec codex2api sh -c \ printf CONNECT api.ipify.org:443 HTTP/1.1\r\nHost: api.ipify.org:443\r\n\r\n | nc -w 5 host.docker.internal 7890正常返回HTTP/1.1 200 Connection established则说明Codex2API ↓ 172.28.0.1:7890 ↓ socat ↓ 127.0.0.1:7890 ↓ HTTP Proxy整条 TCP / HTTP CONNECT 链路已经建立。10. 最终根因.env中代理变量没有成为有效运行时配置最开始.env中配置了CODEX_PROXY_URLhttp://host.docker.internal:7890同时docker exec codex2api sh -c echo $CODEX_PROXY_URL也能够得到http://host.docker.internal:7890这只能证明.env ↓ Docker Compose ↓ 容器环境变量这一步成功。但这并不能证明 Codex2API 当前运行时真正使用了该代理。查询 Codex2API 管理配置curl --noproxy * \ http://127.0.0.1:8180/api/admin/settings \ -H X-Admin-Key: ADMIN_SECRET发现{ proxy_url: }也就是说当前 Codex2API 真正使用的SystemSettings.ProxyURL是空值。当前运行版本中全局代理属于数据库中的运行时业务配置PostgreSQL ↓ SystemSettings ↓ ProxyURL而不是仅依赖CODEX_PROXY_URL环境变量。因此此前实际链路为.env CODEX_PROXY_URLhttp://host.docker.internal:7890 ↓ 成功进入 Docker 环境变量 ↓ 但当前业务运行配置没有使用该值 SystemSettings.ProxyURL ↓ OAuth Token Exchange ↓ Codex2API 直接连接 OpenAI ↓ HTTP 403这才是本次 OAuth Token 兑换失败的核心原因。11. 设置真正生效的 Codex2API 全局代理通过管理 API 写入curl --noproxy * -sS -X PUT \ http://127.0.0.1:8180/api/admin/settings \ -H X-Admin-Key: ADMIN_SECRET \ -H Content-Type: application/json \ -d {proxy_url:http://host.docker.internal:7890}然后验证curl --noproxy * -sS \ http://127.0.0.1:8180/api/admin/settings \ -H X-Admin-Key: ADMIN_SECRET \ | jq -r .proxy_url正确结果http://host.docker.internal:7890这才说明 Codex2API 当前真正启用了全局代理。重新执行 OAuth 后授权码能够正常兑换 Token。12. 为什么.env中的CODEX_PROXY_URL没有生效这里需要区分Docker 环境变量CODEX_PROXY_URLhttp://host.docker.internal:7890能够通过docker exec codex2api sh -c echo $CODEX_PROXY_URL看到只代表变量已经进入容器。Codex2API 实际运行配置真正决定请求是否使用代理的是SystemSettings.ProxyURL查询GET /api/admin/settings返回proxy_url:说明程序实际运行时全局代理为空。因此echo $CODEX_PROXY_URL不能作为当前版本代理是否真正生效的判断依据。正确判断方式是GET /api/admin/settings ↓ proxy_url只有proxy_url:http://host.docker.internal:7890才表示当前全局代理真正生效。13. 最终网络结构最终工作链路为OpenAI / Internet ↑ 代理出口 ↑ 宿主机本地代理 127.0.0.1:7890 ↑ socat ↑ 172.28.0.1:7890 ↑ host.docker.internal ↑ Codex2API 172.28.0.4从 Codex2API 的视角proxy_url ↓ http://host.docker.internal:7890 ↓ extra_hosts ↓ host.docker.internal 172.28.0.1 ↓ socat ↓ 172.28.0.1:7890 ↓ 127.0.0.1:7890 ↓ 宿主机代理 ↓ OpenAI14. 最终 Docker Compose 配置Codex2API 服务services: codex2api: image: ghcr.io/james-6-23/codex2api:latest container_name: codex2api ports: - ${BIND_HOST:-0.0.0.0}:${CODEX_PORT:-8080}:${CODEX_PORT:-8080} env_file: - .env extra_hosts: - host.docker.internal:172.28.0.1 volumes: - image-assets:/data - ./logs:/app/logs depends_on: postgres: condition: service_healthy redis: condition: service_healthy healthcheck: test: [CMD-SHELL, wget -q -O - http://127.0.0.1:$${CODEX_PORT:-8080}/health /dev/null || exit 1] interval: 10s timeout: 3s retries: 6 start_period: 60s stop_grace_period: 60s restart: unless-stopped networks: - codex2api-net15. 固定 Docker 网络当前172.28.0.1是 Docker 网络codex2api_codex2api-net的 Gateway。如果 Docker 将来重新创建网络并自动分配其他网段例如172.29.0.0/16则172.28.0.1可能失效。建议显式固定网络networks: codex2api-net: ipam: config: - subnet: 172.28.0.0/16 gateway: 172.28.0.1这样以下三者始终保持一致Docker Gateway ↓ 172.28.0.1 extra_hosts ↓ host.docker.internal 172.28.0.1 socat ↓ bind172.28.0.116. 常用命令速查查看 Docker 网络docker network inspect codex2api_codex2api-net查看host.docker.internaldocker exec codex2api getent hosts host.docker.internal正常172.28.0.1 host.docker.internal后台启动 socatsudo sh -c nohup socat TCP-LISTEN:7890,bind172.28.0.1,reuseaddr,fork TCP:127.0.0.1:7890 /dev/null ./codex2api-socat.log 21 查看 socat 进程ps -ef | grep [s]ocat查看 7890 监听ss -lntp | grep :7890正常127.0.0.1:7890 172.28.0.1:7890查看日志cat ./codex2api-socat.log实时查看日志tail -f ./codex2api-socat.log测试 Docker 到代理docker exec codex2api sh -c \ nc -z -w 3 host.docker.internal 7890; echo exit$?正常exit0停止 socatsudo pkill -f socat TCP-LISTEN:7890,bind172.28.0.1查询 Codex2API 当前实际代理curl --noproxy * -sS \ http://127.0.0.1:8180/api/admin/settings \ -H X-Admin-Key: ADMIN_SECRET \ | jq -r .proxy_url正常http://host.docker.internal:7890设置 Codex2API 全局代理curl --noproxy * -sS -X PUT \ http://127.0.0.1:8180/api/admin/settings \ -H X-Admin-Key: ADMIN_SECRET \ -H Content-Type: application/json \ -d {proxy_url:http://host.docker.internal:7890}17. 长期运行建议目前使用nohup socat ...可以保证SSH 退出 → socat 继续运行但是服务器重启以后socat不会自动恢复。如果 Codex2API 是长期运行服务建议后续将 socat 配置为systemd服务以获得开机自动启动 异常自动重启 统一启动和停止 统一日志管理此外建议固定 Docker 子网172.28.0.0/16避免 Docker 网络重新创建后 Gateway 改变导致extra_hosts socat bind 地址 proxy_url之间的对应关系失效。18. 最终结论本次 OAuth Token 兑换失败实际上涉及两个独立问题。第一Docker 容器无法直接访问宿主机仅监听于127.0.0.1:7890的代理。通过extra_hosts: - host.docker.internal:172.28.0.1建立容器到宿主机的地址映射再通过socat建立172.28.0.1:7890 → 127.0.0.1:7890的 TCP 转发解决了 Docker 容器访问宿主机代理的问题。第二也是最终导致 OAuth 403 的核心问题虽然.env中存在CODEX_PROXY_URLhttp://host.docker.internal:7890并且环境变量已经进入容器但当前 Codex2API 实际运行使用的是数据库中的SystemSettings.ProxyURL而查询发现proxy_url:因此 OAuth Token Exchange 实际没有使用宿主机代理。通过管理 API 显式设置{ proxy_url: http://host.docker.internal:7890 }后真实链路变为Codex2API ↓ host.docker.internal:7890 ↓ 172.28.0.1:7890 ↓ socat ↓ 127.0.0.1:7890 ↓ 宿主机代理 ↓ OpenAI OAuth最终 OAuth 授权码成功兑换 Token问题解决。因此当前版本中判断 Codex2API 全局代理是否真正生效应以GET /api/admin/settings → proxy_url为准而不能仅根据echo $CODEX_PROXY_URL判断。