Codex ImageGen is a small OpenAI-compatible local/remote image proxy. It exposes /v1/images/generations and /v1/images/edits, uses Codex/ChatGPT OAuth credentials behind the scenes, rotates multiple accounts through an auth pool, and includes a password-gated browser management UI.
The intended deployment is Docker or Portainer with a persistent /data mount for OAuth credentials. Do not bake OAuth tokens into the image.
ghcr.io/ngojclee/codex-imagegen:latest
If the GHCR package is private, configure registry auth in Portainer or run docker login ghcr.io with a GitHub PAT that has read:packages.
Before exposing this service outside a trusted network:
- Set strong unique values for
PROXY_API_KEY,MANAGEMENT_PASSWORD, andMANAGEMENT_SESSION_SECRET. - Keep
ADMIN_API_KEYempty unless you specifically need/admin/*API automation. - Do not reuse
PROXY_API_KEYasADMIN_API_KEY. - Put
/managementbehind VPN, Tailscale, Cloudflare Access, or a reverse proxy with HTTPS. - Treat everything under
/dataas sensitive because it stores OAuth refresh tokens. - Never commit
.env,.env.docker,auth.json,/data,output,.runtime,.omc, or.omx. - OAuth callback URLs contain one-time
codevalues. Paste them only into your own management UI.
Security hardening included in the app:
- Management UI uses an HttpOnly signed cookie.
- Management mutation endpoints require an internal CSRF header.
/admin/*is disabled unlessADMIN_API_KEYis set.- Docker image runs as the non-root
nodeuser. - Compose/Portainer examples use
read_only,tmpfs: /tmp, andno-new-privileges. npm auditcurrently reports 0 vulnerabilities.
Assuming your Docker host is 192.0.2.101 and port 3456:
| Purpose | URL |
|---|---|
| Management UI | http://192.0.2.101:3456/management |
| Health check | http://192.0.2.101:3456/health |
| Text-to-image | http://192.0.2.101:3456/v1/images/generations |
| Image edits | http://192.0.2.101:3456/v1/images/edits |
The root / is not the GUI. Use /management.
Use deploy/portainer-stack.yml and deploy/portainer.env.example.
-
On the Docker host, create the persistent data directory:
mkdir -p /home/Docker/codex-imagegen/data chown -R 1000:1000 /home/Docker/codex-imagegen/data chmod 700 /home/Docker/codex-imagegen/data
The container runs as UID
1000(node). If your NAS/host uses a different permission model, make sure the container can write to this directory. -
In Portainer, go to Stacks → Add stack.
-
Paste the contents of
deploy/portainer-stack.ymlinto the Web editor. -
Paste variables from
deploy/portainer.env.exampleinto Environment variables. -
Change at least these values:
PROXY_API_KEY=sk-proxy-use-a-long-random-value MANAGEMENT_PASSWORD=use-a-strong-password MANAGEMENT_SESSION_SECRET=use-a-long-random-secret
-
If GHCR pull fails with
unauthorized, add a Portainer registry:- Registry URL:
ghcr.io - Username: your GitHub username
- Password: GitHub PAT with
read:packages
Or make the GHCR package public.
- Registry URL:
-
Deploy the stack.
-
Open:
http://<docker-host>:3456/management
The Portainer stack uses this bind mount by default:
/home/Docker/codex-imagegen/data:/data
Inside the container:
| Container path | Purpose |
|---|---|
/data/auth.json |
Optional source auth file for Harvest |
/data/pool/accounts |
Imported account auth files |
/data/pool/events.log |
Pool events log |
/data/output |
Runtime output directory |
Create .env.docker from .env.docker.example and edit the secrets:
cp .env.docker.example .env.dockerRun the published image:
docker pull ghcr.io/ngojclee/codex-imagegen:latest
docker run -d --name codex-imagegen \
-p 3456:3456 \
--env-file .env.docker \
--read-only \
--tmpfs /tmp \
--security-opt no-new-privileges:true \
-v /home/Docker/codex-imagegen/data:/data \
ghcr.io/ngojclee/codex-imagegen:latestOr build locally:
docker compose up -d --buildEnable the UI by setting MANAGEMENT_PASSWORD. Then open:
http://<host>:3456/management
The management UI can:
- show pool stats and account health
- create a manual OAuth login link
- import raw
auth.json - harvest
/data/auth.jsonorCODEX_AUTH_PATH - revive or delete pool accounts
This works well for Docker/Portainer because the browser login happens on your own computer.
-
Open
/managementand sign in. -
Click Create Login Link.
-
Open or copy the generated OpenAI OAuth URL.
-
Login in your browser.
-
Browser redirects to
OAUTH_REDIRECT_URI, default:http://localhost:1455/auth/callbackIt is fine if the page says it cannot connect. You only need the URL in the browser address bar.
-
Copy the full callback URL, including
code=...&state=.... -
Paste it into Callback URL.
-
Click Complete Login.
The server exchanges the code using PKCE, creates a Codex-compatible auth JSON, and imports it into /data/pool/accounts.
If you already have a Codex OAuth file, paste or upload it in the Import Auth panel.
Expected Codex format:
{
"auth_mode": "chatgpt",
"tokens": {
"access_token": "...",
"refresh_token": "...",
"id_token": "...",
"account_id": "..."
}
}Copy an auth file to the data directory as:
/home/Docker/codex-imagegen/data/auth.json
Then click Harvest in the management UI.
All image endpoints require:
Authorization: Bearer <PROXY_API_KEY>curl -X POST http://localhost:3456/v1/images/generations \
-H "Authorization: Bearer sk-proxy-use-a-long-random-value" \
-H "Content-Type: application/json" \
-d '{"prompt":"a cinematic fantasy city at sunrise"}'Request body fields:
| Field | Type | Required | Notes |
|---|---|---|---|
prompt |
string | yes | Text prompt |
model |
string | no | Defaults to DEFAULT_MODEL |
input_refs |
string[] | no | Data URLs or HTTP image URLs |
timeout_ms |
number | no | Defaults to DEFAULT_TIMEOUT_MS |
size |
string | no | Forwarded to image tool |
quality |
string | no | Forwarded to image tool |
background |
string | no | Forwarded to image tool |
n |
number | no | Accepted for compatibility, ignored; returns one image |
curl -X POST http://localhost:3456/v1/images/edits \
-H "Authorization: Bearer sk-proxy-use-a-long-random-value" \
-F "prompt=Transfer this nail-art pattern onto the target nails" \
-F "image[]=@source_artwork.png;type=image/png" \
-F "image[]=@target_nails.png;type=image/png"Upload limits:
- max 10 MB per file
- max 5 multipart files total
image[]orimageacceptedmaskaccepted but currently ignored
{
"created": 1234567890,
"request_id": "uuid",
"data": [
{
"b64_json": "iVBORw0KGgo...",
"revised_prompt": "..."
}
],
"usage": {
"elapsed_ms": 12345
}
}b64_json is raw base64 without a data:image/png;base64, prefix.
The browser management UI is preferred. The JSON admin API is disabled unless ADMIN_API_KEY is set.
When enabled, use:
Authorization: Bearer <ADMIN_API_KEY>Endpoints:
| Endpoint | Purpose |
|---|---|
GET /admin/accounts |
List pool accounts |
POST /admin/harvest |
Harvest CODEX_AUTH_PATH |
POST /admin/accounts/:id/revive |
Mark account healthy |
DELETE /admin/accounts/:id |
Remove account from pool |
| Variable | Required | Default | Notes |
|---|---|---|---|
PROXY_API_KEY |
yes for Docker | auto-generated locally | Client API key for image endpoints |
ADMIN_API_KEY |
no | empty | Enables /admin/*; leave empty to disable |
MANAGEMENT_PASSWORD |
yes for UI | empty | Enables /management; empty disables UI |
MANAGEMENT_SESSION_SECRET |
yes for Docker | generated locally | HMAC secret for management cookie |
MANAGEMENT_SESSION_TTL_MS |
no | 43200000 |
12 hours |
MANAGEMENT_COOKIE_SECURE |
no | false |
Set true when serving only through HTTPS |
PORT |
no | 3456 |
Host/container app port |
CODEX_AUTH_PATH |
no | ~/.codex/auth.json or /data/auth.json in Docker |
Harvest source file |
POOL_ROOT |
no | ~/.codex-imagegen or /data/pool in Docker |
Pool root |
ACCOUNTS_DIR |
no | <POOL_ROOT>/accounts |
Account auth files |
EVENTS_LOG |
no | <POOL_ROOT>/events.log |
JSONL event log |
OUTPUT_DIR |
no | ./output or /data/output in Docker |
Runtime output |
CODEX_BASE_URL |
no | https://chatgpt.com/backend-api/codex |
Codex backend base URL |
CODEX_REFRESH_URL |
no | https://auth.openai.com/oauth/token |
OAuth token endpoint |
DEFAULT_MODEL |
no | gpt-5.4 |
Default model |
DEFAULT_TIMEOUT_MS |
no | 900000 |
Request timeout |
COOLDOWN_DEFAULT_MS |
no | 60000 |
Account cooldown after 429/403 |
OAUTH_REDIRECT_URI |
no | http://localhost:1455/auth/callback |
Manual OAuth redirect URI |
OAUTH_SESSION_TTL_MS |
no | 600000 |
OAuth state lifetime |
DATA_DIR |
Portainer only | /home/Docker/codex-imagegen/data |
Host bind mount path |
GitHub Actions runs on every push to master:
- Node syntax checks
docker compose config- portable zip artifact:
codex-imagegen-portable.zip - GHCR Docker image:
ghcr.io/ngojclee/codex-imagegen:latest
If GitHub warns about Node.js 20 actions deprecation, that is from upstream GitHub actions runtime and does not affect this app build today.
Use the current deploy/portainer-stack.yml; it starts with:
version: "3.9"The package is private or your Docker host is not logged in.
Fix options:
- make the GHCR package public, or
- configure Portainer registry
ghcr.iowith a GitHub PAT that hasread:packages, or - run
docker login ghcr.ioon the Docker host.
Set MANAGEMENT_PASSWORD and restart/redeploy.
Use the built-in UI. If calling management API manually, include:
x-codex-imagegen-csrf: 1Add an account using OAuth Login, Import Auth, or Harvest /data/auth.json.
The image runs as UID 1000. Fix host directory permissions, for example:
chown -R 1000:1000 /home/Docker/codex-imagegen/data
chmod 700 /home/Docker/codex-imagegen/dataThat is expected when using the manual flow. Copy the full URL from the browser address bar and paste it into the management UI.
npm install
npm startLocal first run can auto-generate .env with PROXY_API_KEY. Docker deployments should set secrets explicitly.
In Docker/production (NODE_ENV=production), PROXY_API_KEY is required. If MANAGEMENT_PASSWORD is set, MANAGEMENT_SESSION_SECRET is also required.
MIT