A small FastAPI wrapper around Codex CLI's $imagegen.
Issue bearer keys to your internal scripts, CI jobs, and side projects;
they all share one ChatGPT subscription's image-gen quota over a clean
HTTP endpoint.
You already pay for ChatGPT. Codex CLI installed on your homelab box is
already logged in to it. But every internal script, CI workflow, or
Discord/Telegram bot that wants to generate an image has to either
(a) ssh into your box and shell out to codex
exec, or (b) burn separate OpenAI Images API credits.
This service does (c): expose $imagegen over
a tiny HTTP API behind Authorization: Bearer
cimg_<random-token>, with an admin UI to issue / revoke keys per caller.
One ChatGPT account, many consumers, no extra spend.
medium.
Under the hood it's gpt-image-2 (the model
behind Codex CLI's built-in image_gen tool).
The output is whatever $imagegen produces —
this service does not reprocess or filter; it just delivers the
PNG to the caller and tracks history.
Prerequisites: Codex CLI
installed and codex login done on the host.
Docker + Docker Compose for the containerized path.
A. Local testing — no nginx
git clone https://github.com/yazelin/codex-image-service cd codex-image-service cp .env.example .env # edit .env: set ADMIN_PASSWORD and ADMIN_SESSION_SECRET docker compose -f docker-compose.local.yml up -d --build open http://localhost:8000/admin
Maps port 8000 directly to your host, no reverse proxy required.
Image URLs in API responses come back as
http://localhost:8000/generated/....
B. Production — behind your existing nginx
# .env: set ADMIN_PASSWORD, ADMIN_SESSION_SECRET, # PUBLIC_BASE_URL=https://YOUR-DOMAIN/codex-image, # ADMIN_URL_PREFIX=/codex-image docker compose up -d --build # then paste deploy/nginx.codex-image-service.location.conf.example # into your existing nginx and reload
The default docker-compose.yml attaches the
container to a pre-existing Docker network called
nginx_bridge_network. First-run smoke test:
curl -sf https://YOUR-DOMAIN/codex-image/health # {"status":"ok"}
C. No Docker — fastest dev loop
python3 -m venv .venv && . .venv/bin/activate pip install -r requirements.txt uvicorn app.main:app --reload --port 8000
Uses your host's own ~/.codex/auth.json
directly. Easiest for iterating on the code.
Log in to /codex-image/admin, click
Create API Key, copy the cimg_<random-token>
value. Refresh or leave the page and the raw value is gone forever —
only the sha256 hash stays on the server. Then hit the API:
# shell / GitHub Actions curl -sS --fail --max-time 650 \ -X POST https://YOUR-DOMAIN/codex-image/v1/images/generate \ -H "Authorization: Bearer $CODEX_IMAGE_KEY" \ -H "Content-Type: application/json" \ -d '{"prompt":"...","size":"1024x1024","quality":"medium","count":1}'
# python (httpx) import httpx, os r = httpx.post( "https://YOUR-DOMAIN/codex-image/v1/images/generate", headers={"Authorization": f"Bearer {os.environ['CODEX_IMAGE_KEY']}"}, json={"prompt": "...", "size": "1024x1024", "quality": "medium", "count": 1}, timeout=httpx.Timeout(connect=10, read=600, write=30, pool=10), ) images = r.json()["images"] # [{"url": "...", "expires_at": "..."}]
Response shape:
{
"id": "img_3f81...",
"status": "succeeded",
"images": [{"url": "https://YOUR-DOMAIN/codex-image/generated/img_3f81....png",
"expires_at": "..."}],
"created_at": "..."
}
A single ChatGPT subscription's per-account image-gen quota is the real
cap on throughput. Configure two or more ChatGPT accounts and the service
rotates CODEX_HOME between them per request,
with automatic cross-account retry if one account errors.
# one host dir per account under ~/codex-homes/<label>/ mkdir -p ~/codex-homes/{personal,team} CODEX_HOME=~/codex-homes/personal codex login # log in with ChatGPT A CODEX_HOME=~/codex-homes/team codex login # log in with ChatGPT B # .env (paths are *inside the container*) CODEX_HOMES=/host_codex_homes/personal:/host_codex_homes/team # restart — the parent ~/codex-homes is already mounted into /host_codex_homes docker compose up -d --build
Label the folders anything you want
(personal / team-acme /
dev / …). Two logins on the same
ChatGPT user account share one quota pool, so only distinct user
accounts actually expand capacity.
Admin Overview gets one card per account with the 30-day request count,
success/failure split, auth-token freshness chip (green ≤6d, amber 7–9d,
red ≥10d since last_refresh), and the first
8 chars of the ChatGPT account_id. Access
tokens last ~10 days and the auth.json files are mounted read-only,
so keep them fresh on the host side:
# weekly cron — touch each home so codex refreshes its tokens for h in ~/codex-homes/*/; do CODEX_HOME="$h" codex --version >/dev/null; done
Names go into the api_keys table; tokens stored as sha256 hashes and shown raw exactly once at creation.
Pick a key + prompt + size + quality, queue a job from the admin UI to verify the key works without touching curl.
Each image carries an expires_at (default IMAGE_RETENTION_DAYS=7); a background sweep deletes PNGs + workdirs.
Per-row Delete on the Image Requests table for immediate purge — file, workdir, DB row — without waiting for expiry.
Short live walk-through (19 s, no audio):
This project was built for our own development and testing inside a
private homelab. It is not affiliated with, endorsed by, or
supported by OpenAI. It wraps the official
@openai/codex CLI and re-exposes its
$imagegen skill as a small HTTP API; every
request consumes quota from the single ChatGPT account whose
~/.codex/auth.json is mounted into the container.
Things to be aware of before reusing this code:
$imagegen skill, the model behind it (gpt-image-2), the sandbox flags, or the on-disk layout at any time. This service may need follow-up patches.app/services/codex_image.py — it runs codex with --dangerously-bypass-approvals-and-sandbox because bubblewrap doesn't work inside Docker — and re-think the threat model before pointing it at anything important.