Codex2API Docker 使用宿主机代理:OAuth Token 兑换 403 问题排查与解决方案

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'

此处故意提供错误的 codeclient_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

判断。

相关推荐
晨枫阳2 小时前
@changesets/cli是什么?哪些情况下需要使用?怎么使用
linux·运维·ubuntu
做前端的娜娜子2 小时前
Docker 常用命令全梳理:从镜像拉取到容器编排
docker·容器·掘金·金石计划
天远Date Lab2 小时前
零信任架构实战:基于天远行驶证核查构建自动化车队准入网关
运维·人工智能·架构·自动化
不会就选b2 小时前
Linux之网络基础(二)
linux·运维·网络
莫浅子3 小时前
Day 4:USB 2.0 时序与带宽预算
linux·运维·网络
ly76893 小时前
Linux 从入门到实践:系统架构、常用命令、服务管理与故障排查详解
linux·运维·系统架构
青瓦梦滋4 小时前
IP/MAC帧/ARP协议
运维·服务器·网络·网络协议·tcp/ip
阿 才5 小时前
Matlab(Simulink)使用详解
运维·网络·matlab
St_rive5 小时前
webUI自动化实现及封装
运维·自动化