Codex2API Docker 使用宿主机代理:OAuth Token 兑换 403 问题排查与解决方案
1. 问题描述
在 Linux 服务器上通过 Docker Compose 部署 Codex2API,宿主机已经配置本地代理:
export http_proxy=http://127.0.0.1:7890
export https_proxy=http://127.0.0.1:7890
export all_proxy=socks5://127.0.0.1:7891
Codex2API 的 .env 中同时配置:
CODEX_PROXY_URL=http://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 Endpoint:
curl -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_type=authorization_code&code=invalid&client_id=invalid&redirect_uri=http://localhost'
此处故意提供错误的 code 和 client_id。
如果能够收到 OpenAI 返回的标准 JSON 错误,例如 HTTP 401,而不是连接超时、拒绝连接或代理错误,则说明:
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,bind=172.28.0.1,reuseaddr,fork \
TCP:127.0.0.1:7890
参数含义:
TCP-LISTEN:7890
监听 TCP 7890 端口。
bind=172.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,bind=172.28.0.1,reuseaddr,fork TCP:127.0.0.1:7890 </dev/null > ./codex2api-socat.log 2>&1 &'
其中:
nohup
表示 SSH 会话退出后进程继续运行。
</dev/null
禁止后台进程继续读取当前终端,避免出现:
Stopped (tty output)
> ./codex2api-socat.log 2>&1
将标准输出和错误输出统一写入:
./codex2api-socat.log
&
表示后台运行。
检查进程:
ps -ef | grep '[s]ocat'
正常类似:
root 3186222 1 ... socat TCP-LISTEN:7890,bind=172.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
停止 socat
sudo pkill -f 'socat TCP-LISTEN:7890,bind=172.28.0.1'
停止后:
ss -lntp | grep ':7890'
正常只剩:
127.0.0.1:7890
9. 测试 Docker 到宿主机代理
首先测试 TCP 连通性:
docker exec codex2api sh -c \
'nc -z -w 3 host.docker.internal 7890; echo "exit=$?"'
如果返回:
exit=0
说明:
Codex2API
↓
host.docker.internal
↓
172.28.0.1:7890
已经连通。
进一步测试 HTTP CONNECT:
docker 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_URL=http://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_URL=http://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_URL=http://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
↓
宿主机代理
↓
OpenAI
14. 最终 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-net
15. 固定 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
↓
bind=172.28.0.1
16. 常用命令速查
查看 Docker 网络
docker network inspect codex2api_codex2api-net
查看 host.docker.internal
docker exec codex2api getent hosts host.docker.internal
正常:
172.28.0.1 host.docker.internal
后台启动 socat
sudo sh -c 'nohup socat TCP-LISTEN:7890,bind=172.28.0.1,reuseaddr,fork TCP:127.0.0.1:7890 </dev/null > ./codex2api-socat.log 2>&1 &'
查看 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=$?"'
正常:
exit=0
停止 socat
sudo pkill -f 'socat TCP-LISTEN:7890,bind=172.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_URL=http://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
判断。