CLIProxyAPI + Codex Setup: Docker, OAuth & Reverse Proxy Guide
Companion repo: the one-command deploy version of this guide — Docker deployment, key generation, and config rendering in one command, plus a client-config generator and an Nginx example:
kuangyongjie/cliproxyapi-codex-deploy
git clone https://github.com/kuangyongjie/cliproxyapi-codex-deploy.git
cd cliproxyapi-codex-deploy
./scripts/setup.sh
CLIProxyAPI can put Codex behind your own OpenAI-compatible endpoint. The practical path is: run CLIProxyAPI with Docker, complete Codex OAuth on the proxy, expose a stable /v1 endpoint, then point the local Codex client at that endpoint with the Responses API enabled.
Quick setup
- Deploy CLIProxyAPI with Docker or Docker Compose.
- Persist the proxy auth directory so OAuth state survives restarts.
- Run the Codex OAuth login flow on the proxy.
- Expose the proxy through HTTPS while preserving the
/v1/path. - Point Codex at the proxy with a custom
base_url. - Set
wire_api = "responses"and use a downstream proxy key.
The rest of this guide explains why each step matters, how the auth layers fit together, and the failure modes that most often break an otherwise healthy setup.
If Codex has already become part of your daily workflow, there is a fairly obvious question that starts showing up sooner or later: do you just keep using the official direct login flow, or do you put Codex behind your own proxy layer and make it part of a broader setup you control?
For one machine and one user, direct login is still the easiest answer. There is no point pretending otherwise. You install it, sign in, and move on. Fewer moving parts, fewer services, fewer places to debug.
But once your workflow grows past that, the question changes. Maybe you want multiple machines to point at the same endpoint. Maybe you want Codex, Claude Code, and Gemini CLI to sit behind a single routing layer. Maybe you want OAuth state and access policy handled server-side instead of redoing everything on each client. That is where CLIProxyAPI stops looking like a niche proxy project and starts looking like a useful piece of infrastructure.
What pushed me to take this path seriously was not “I want a fancier setup.” It was realizing that direct login is great for one machine, but starts to feel scattered once Codex needs to live alongside a VPS, a few self-hosted tools, and some internal scripts. Login state stays on one client, every other app wants its own endpoint config, and the next consumer means another round of setup. So the real question became: if you want Codex behind CLIProxyAPI, running with Docker on a Linux server, authenticated through OpenAI OAuth there, and then wired back into the local Codex app, what does that chain actually look like, and where does it usually go wrong?
Why Put Codex Behind a Proxy
My answer is basically this: if your goal is immediate convenience, direct login still wins. If your goal is to make Codex part of a reusable, controllable AI tooling layer, the proxy approach is worth it. The value is not that it looks more advanced. The value is that authentication state, routing, endpoint control, and later expansion stop being tied to a single client install.
And once that proxy layer exists, the benefit usually spills far beyond Codex itself. What you really gain is a reusable OpenAI-compatible entry point that other self-hosted tools can share too. If a project can point to a custom base_url, there is a good chance it can sit behind the same gateway. Codex may be the first thing you put there, but it rarely stays the only one. A self-hosted frontend, an agent panel, an internal script service, or another OpenAI-compatible app can usually reuse the same endpoint instead of each one carrying its own upstream login state.
What is easy to miss here is what actually gets shared. It is not that you keep copying one upstream secret into more places. It is the opposite. The OAuth and token state stays centralized inside the proxy layer, and your downstream tools only need the proxy URL plus a proxy-issued key. That sounds like a small config change, but operationally it is a very different shape from redoing login and secret placement in every app or container. The next time you add another OpenAI-compatible tool, you are not asking “how do I log this one in too?” You are asking “which proxy key should it use, and should it share the same endpoint?”
That is a big deal for an independent developer, because it changes the shape of secret management. Instead of every machine and every app needing to know about upstream auth, the sensitive OAuth state stays centralized in the proxy layer. Downstream clients and self-hosted projects only need a proxy key and a stable endpoint. Adding a new project becomes “issue a new downstream key and point it at the gateway,” not “repeat the whole upstream auth story again.”
There is also a very practical middle ground here. If you only have a handful of internal tools today, you can let several of them share one proxy API key at first just to get everything online quickly. Later, when you want cleaner logging, per-project throttling, or the ability to revoke one consumer without touching the others, you split those keys out. In other words, the proxy layer is useful both when you are optimizing for speed now and when you are preparing for a more disciplined setup later.
Split the Chain Into Three Parts
What helped me most was separating the whole thing into three pieces. First, there is the CLIProxyAPI service running on the server. Second, there is the Codex OAuth state that must actually be stored in the mounted auth directory used by that service. Third, there is the local Codex client configuration, mainly ~/.codex/config.toml and ~/.codex/auth.json. If those three layers stay distinct in your head, the setup is manageable. If they get blurred together, the whole thing starts to feel more mysterious than it actually is.
The project documentation already covers the main moving parts: Codex via OAuth, Docker and Docker Compose deployment, and the local client configuration pattern. The trick is understanding which file belongs to which layer. config.yaml belongs to the proxy service. The mounted auth/ directory belongs to the proxy’s stored login state. The files under ~/.codex/ belong to the local Codex app.
Server Side: Docker Deployment and OAuth Login
If I were setting this up from scratch, I would keep the server directory extremely small:
/opt/cliproxyapi/
├── docker-compose.yml
├── config.yaml
└── auth/
That auth/ directory is more important than it looks. CLIProxyAPI defaults to ~/.cli-proxy-api as its auth directory, and with Docker the whole point is to mount that path out of the container so OAuth state survives restarts and container rebuilds. A lot of “it worked yesterday and broke after restart” cases are really just missing persistence here.
For the first pass, I would also keep config.yaml minimal. The official config.example.yaml is large, but for a Codex proxy setup the pieces that matter first are pretty small:
host: ""
port: 8317
auth-dir: "~/.cli-proxy-api"
api-keys:
- "<proxy-api-key>"
remote-management:
allow-remote: false
secret-key: ""
Two things are easy to misread here. The api-keys section is not your OpenAI upstream credential. It is the credential used by downstream clients when they call the proxy. That split is actually one of the best parts of the design, because you can issue different downstream keys to different clients or projects. Codex can use one, another self-hosted app can use another, and if one needs to be rotated or revoked you do it at the proxy layer instead of revisiting every upstream login. And unless you have a real reason to expose the management layer remotely, I would leave remote-management.allow-remote off. The default is already conservative, which is the right default for this use case.
For Docker Compose, I would keep it simple and explicit:
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
Both ports are bound to loopback on purpose. The API port only needs to be reachable by Nginx on the same host — the Client Side section below proxies to http://127.0.0.1:8317 — and the OAuth callback listener has a documented history of binding all interfaces when published openly (upstream issue #3463). Publishing these two ports to the public internet by default would turn a temporary login surface into a permanent one, so do not treat public publishing as the safe default. If your network topology genuinely needs wider access, define that as an explicit exception with its own restrictions, not as the starting point.
One more note on the image tag: :latest is a moving tag. The v8 field map in this guide was verified against the upstream repository’s main branch on 2026-10-14 — it was not verified by running this image. Before you deploy, confirm the tag you pull still matches that layout, or pin a digest you have checked yourself.
That extra 1455 port matters for a reason. The official provider docs note that Codex OAuth uses a local callback on port 1455. People often focus only on the main API port 8317, but the OAuth part of the flow has its own expectations. The service can be up and healthy while the login path still fails because that callback path was never accounted for.
There is one thing about that callback that is easy to get wrong on a remote machine. The browser runs on your local laptop, so http://localhost:1455/auth/callback resolves to your laptop — not to the VPS — no matter how the container’s ports are mapped. For a remote Linux/VPS setup, the order that actually closes the loop is:
- On your local machine (not the server), open the tunnel first:
ssh -N -L 1455:127.0.0.1:1455 <user>@<vps> - Run the login command from this guide. The CLI prints the OAuth URL — with
-no-browser, and its own source even prints these SSH tunnel instructions for you. - Open that URL in your local browser and complete the sign-in. The callback to
localhost:1455now travels through the tunnel back to the container.
If you skip the tunnel, the CLI will sit at “Waiting for Codex authentication callback…” and, after about 15 seconds, offer Paste the Codex callback URL (or press Enter to keep waiting):. You can paste the full callback URL straight from your browser’s address bar as the fallback — that is the CLI’s own manual path, not a workaround.
Once the container is up, the documented Codex login command is:
docker compose exec cli-proxy-api /CLIProxyAPI/CLIProxyAPI -no-browser --codex-login
The -no-browser flag is especially helpful on a remote Linux box because it prints the URL instead of trying to launch a browser in the container. That is also the point where it helps to be very precise mentally: you are not signing the local Codex client in at this stage. You are authenticating Codex for the proxy service so the proxy can answer Codex-compatible requests later.
If this step does not really complete, nothing after it matters much. So the first time through, I would watch two things closely: the output in docker compose logs -f, and the mounted auth/ directory. If the OAuth flow succeeded but nothing useful landed in auth/, you still do not have a working proxy state.
config.yaml Field Reference
The minimal config.yaml earlier in this guide uses the older top-level field spellings (host, port, auth-dir, api-keys, remote-management). Those legacy spellings still load fine, but the upstream project has moved to a v8 layout (config-version: 8) where every setting lives under a named section. If you copy examples from the current config.example.yaml in the upstream repository, you will see the new layout — so here is a field-by-field map for the settings a Codex proxy setup actually touches. Every field name below is verified against the upstream config.example.yaml on the main branch (checked 2026-10-14). Plus forks may diverge; confirm their pinned image/version and config.example.yaml before assuming this layout applies to them.
| Field (v8) | Legacy spelling | What it does |
|---|---|---|
config-version: 8 | — | Declares the v8 layout. When both spellings of a field are present, the v8 value wins. |
server.host | host | Interface to bind. An empty string binds all interfaces. |
server.port | port | The API port clients call. 8317 in this guide. |
oauth.auth-dir | auth-dir | Where Codex OAuth state is stored. It must match the auth/ volume mount, or logins vanish on restart. |
access.api-keys | api-keys | Downstream keys your clients present. These are not upstream OpenAI credentials. |
management.allow-remote | remote-management.allow-remote | Keep false unless you have a real reason to expose the management API. |
management.secret-key | remote-management.secret-key | Key for the management API. Leaving it empty disables the management API entirely. |
client.codex.optimize-multi-agent-v2 | — | Optional. Optimizes Codex Desktop and codex-tui requests for multi-agent setups. Default false. |
A v8-style minimal config for this guide’s setup looks like this:
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: ""
Two details are worth calling out. First, access.api-keys is the same list as the legacy api-keys: keys you hand to downstream clients, never upstream secrets. Second, oauth.auth-dir inside the container resolves to /root/.cli-proxy-api by default, which is exactly why the compose file mounts ./auth there. Change one side without the other and the “worked yesterday, broke after restart” failure from the previous section comes straight back.
Docker Compose: The Standalone Path
The companion repo linked at the top of this guide automates everything below into one script. If you would rather own every file and see exactly what runs, here is the same setup as a standalone compose project — no helper scripts involved.
Keep the same small directory:
/opt/cliproxyapi/
├── docker-compose.yml
├── config.yaml
└── auth/
The compose file is the same one from the Server Side section above — same 127.0.0.1-bound ports, same mounts, assembled from the upstream repository’s own docker-compose.yml and trimmed to what this guide needs. I am not repeating it here because there should be exactly one file to keep correct; copy it from above. The ./auth mount still has to land on the path oauth.auth-dir points to, or OAuth state is lost on every container rebuild.
Bring it up and watch the startup:
docker compose config
docker compose up -d
docker compose logs -f
That first command is worth a habit: docker compose config renders the effective file without starting anything, a free static check that catches indentation slips and wrong mount paths before they become runtime mysteries.
Then run the Codex login exactly as in the previous section:
docker compose exec cli-proxy-api /CLIProxyAPI/CLIProxyAPI -no-browser --codex-login
One honest caveat: every field in the compose file above is verified against the upstream repository’s docker-compose.yml and the configuration already shown in this guide, but I did not execute this exact file on a live machine for this update. If your environment differs — rootless Docker, a non-default data root — the volume paths are the first thing to double-check.
Client Side: Codex Client and Nginx Configuration
This leads to the part that feels slightly counterintuitive at first. Even after the proxy has successfully completed Codex OAuth, the local Codex app still needs to be reconfigured. According to the official CLIProxyAPI Codex client page, that means editing ~/.codex/config.toml and ~/.codex/auth.json.
It helps to think of this as two separate key layers. Upstream, the proxy holds the real OAuth state and token material that lets it act on behalf of your Codex access. Downstream, each client or app only needs permission to use your proxy. That split is exactly why this setup becomes useful once more than one tool is involved. You do upstream auth once, then hand out downstream access in a controlled way.
For a remote server behind your own domain, I would treat config.toml as the place where you tell Codex to stop thinking “direct OpenAI” and start thinking “use my proxy provider instead”:
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"
Two mistakes show up a lot here. The first is leaving base_url pointed at a localhost example even though the proxy actually lives on a remote server. The second is forgetting wire_api = "responses". The CLIProxyAPI docs explicitly use the responses wire shape for Codex, so dropping that line is one of the easiest ways to create weird client behavior that looks harder to diagnose than it really is.
Then there is auth.json, which the official docs keep intentionally minimal:
{
"OPENAI_API_KEY": "<proxy-api-key>"
}
That <proxy-api-key> value tends to make people pause, but the pause should be about which key to put there, not whether it matters. It has to be one of the keys in your config.yaml’s api-keys list — the downstream proxy key from step 6 of the quick setup. CLIProxyAPI treats those keys as client authentication credentials, so this is what the local Codex client presents when it talks to your proxy. It is still not a real upstream OpenAI key; the routing logic lives in config.toml, especially the provider block and the proxy base_url.
If you also want to front the service with Nginx and put HTTPS in front of it, the proxying itself is straightforward. The part I would not get clever with is the /v1/ path. Keep it intact and avoid unnecessary rewriting:
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;
}
}
Once that endpoint is stable, its usefulness goes well beyond the Codex desktop app. A lot of self-hosted tools only need an OpenAI-compatible base URL and an API key-like credential. At that point your setup stops being “a special Codex proxy” and starts becoming “my unified AI gateway.” The environment variables those projects expect often look like this:
OPENAI_BASE_URL=https://codex-proxy.example.com/v1
OPENAI_API_KEY=<proxy-api-key-for-this-project>
That is where the architecture really starts paying off. The heavy part, upstream OAuth, account state, provider routing, possible multi-account policies, stays inside CLIProxyAPI. Each downstream project only needs a stable endpoint and its own proxy key. If you later change the upstream, add more accounts, swap routing strategy, or move traffic between tools, you do that in one place instead of touching every client and every app separately.
This becomes even more useful when you already run a few different kinds of tools. Maybe Codex handles your coding workflow, a self-hosted chat frontend handles day-to-day prompting, and a small internal script or agent service handles automation. Without a shared gateway, each of those tends to grow its own secret story. With a proxy in the middle, the long-lived upstream state lives in one place, while the downstream side becomes much simpler: which project gets which proxy key, and which endpoint it should call. That is the point where the setup stops feeling like “a special Codex trick” and starts feeling like part of your normal infrastructure.
The Most Common Failure Points
In real use, the most common failures are usually not about Docker at all. They are things like:
- the proxy container is up, but the mounted
auth/directory never actually received Codex OAuth state - the local client config was edited, but
base_urlstill points to localhost - the domain proxy exists, but
/v1/is missing or changed wire_api = "responses"was skipped- someone assumes
auth.jsonneeds a real upstream OpenAI secret and reintroduces credentials that are not necessary there
That is also the clearest difference between this and direct login. Direct login is simpler because auth and client behavior stay in one place. CLIProxyAPI splits the system into service runtime, stored OAuth state, reverse proxying, and client configuration. That is more setup at the beginning, but it buys you a much more reusable shape once you want Codex to live inside a bigger environment instead of as a one-off local app.
So is it worth it? I think the cleanest answer is this: direct login is better when you optimize for “I want to use Codex right now on this machine.” CLIProxyAPI is better when you optimize for “I want Codex to become part of my own controlled AI tooling layer.” One is lower friction. The other gives you more room to standardize, reuse, and extend later. And that tradeoff becomes much more attractive the moment you have more than one consumer for the same upstream capability. Once a few self-hosted tools start sharing the same AI backend, centralizing auth and downstream keys usually feels much cleaner than scattering secrets and login state across every project.
And once you take that second path, it is worth treating config.yaml and auth/ like real state, not disposable setup files. The part that actually hurts during recovery is rarely pulling the image again. It is rebuilding the small bits of auth and config you assumed were easy to recreate.
If you are already shaping a larger Codex workflow, What Changed in the New Codex: Why It Feels More Like a Dev Environment is a useful companion read. And if this proxy is going to stay on a VPS for a while, Back Up Docker Volumes to GitHub: a Low-Cost Disaster Recovery Setup for 1Panel fits naturally too, because auth and config state stop feeling theoretical the first time you need them back fast.