Deployment and operations¶
The OCI image contains the bridge binary and the pinned Codex CLI, runs as
UID/GID 10001, and uses tini for signal forwarding and child reaping. The
runtime root filesystem is compatible with read-only mode. Only Codex OAuth
state, bridge session state, verified artifacts, and optional input workspace
are mounted.
Why the image includes Codex¶
The default codex-responses path does not spawn Codex for each request. It
reads the mounted ChatGPT OAuth state and calls the Codex Responses backend
directly. The image also carries a checksum-verified, pinned Codex executable
because the supported codex-app-server compatibility path is a child process
owned and supervised by the bridge.
A native bridge installation can reuse the codex executable already on the
user's PATH, or an explicit providers.codex_app_server.executable path. A
container cannot safely execute an unrelated process from its host without an
additional socket or network protocol. Mounting the host executable is also
fragile across architectures, libraries, permissions, and upgrades, so the
public image is intentionally self-contained. When codex-app-server is
disabled, that executable is not used at runtime, although it remains present
in the current general-purpose image.
Prepare Codex OAuth state¶
Use a dedicated writable Codex home rather than mounting an unrelated user's
entire home directory. Codex may rotate OAuth credentials, so auth.json must
remain writable by UID 10001.
install -d -m 0700 ./deploy/codex-home
cp "$HOME/.codex/auth.json" ./deploy/codex-home/auth.json
chown -R 10001:10001 ./deploy/codex-home
Never commit this directory. The repository ignores deploy/codex-home/.
Compose¶
There are three Compose entry points:
| File | Purpose | Image source |
|---|---|---|
compose.package.yaml |
Standalone released deployment | Pulls the pinned GHCR package |
compose.yaml |
Source build and image development | Builds the included Dockerfile |
compose.handoff.yaml |
Single-OAuth-active blue/green handoff | Stable gateway plus released slots |
For a deployment without a repository checkout, follow the Docker quickstart. The commands below are specifically for building from a source checkout:
export IMAGEGEN_BRIDGE_BEARER_TOKEN="use-a-secret-manager-or-random-value"
export IMAGEGEN_BRIDGE_CODEX_HOME="$PWD/deploy/codex-home"
docker compose up --build -d
docker compose ps
Both Compose paths bind the API to host loopback by default, drop every Linux capability, block privilege escalation, limit PIDs, mount the root filesystem read-only, and use bounded tmpfs mounts. They use Compose's default project network. Do not bind the API publicly without bridge bearer authentication and a trusted TLS reverse proxy.
The default layout is:
| Container path | Purpose | Access |
|---|---|---|
| Service configuration | Mounted TOML for source builds; container arguments for the package | read-only |
/codex-home |
Dedicated Codex OAuth state | read/write, secret |
/data/state |
SQLite session bindings and durable job history | read/write, persistent |
/data/artifacts |
Verified bridge-owned outputs | read/write, persistent |
/workspace |
Optional local/reference inputs | read-only |
/tmp, /home/imagegen |
Bounded ephemeral scratch | tmpfs |
The default server.read_timeout_ms = 0 disables connection read-idle expiry.
This is intentional for synchronous image requests that can spend several
minutes inside a handler before producing response bytes. Request bodies remain
bounded by max_body_bytes and intrinsic input limits. A positive value enables
an idle read-stall deadline for deployments that prefer it. The write timeout
is progress-based and starts only when socket output stalls; it does not limit
generation duration.
The Codex Responses provider defaults to two total attempts with a 750 ms base
backoff for failures that are explicitly safe to retry. Set
providers.codex_responses.max_transient_attempts = 1 to disable this recovery.
The accepted range is 1..=2; unknown outcomes are never retried regardless of
configuration.
Active/passive zero-error handoff¶
compose.handoff.yaml exposes only the gateway. The gateway has no Codex home,
OAuth credential, state database, artifact store, or provider process. It reads
only deploy/coord/active-slot, whose value is blue, green, or hold.
Only one bridge slot runs at a time. During an update the updater writes
hold, so the bounded gateway retains new requests without dispatching them.
It gracefully stops the active slot, starts the inactive image, requires deep
provider readiness, switches the file atomically, and releases held requests.
Because the old slot is stopped before the new one starts, the slots never use
OAuth concurrently. Both also contend on server.activation_lock stored in
the shared durable state volume; the cross-process kernel lock is acquired before OAuth discovery or provider
bootstrap and is released automatically on exit or crash. A failed readiness
gate stops the candidate, restores its
pin, restarts the old slot, and routes held requests back to it.
Initial start:
install -d deploy/coord
printf 'blue\n' > deploy/coord/active-slot
docker compose -f compose.handoff.yaml up -d gateway blue
Upgrade after reviewing the dry-run:
imagegen-bridge update docker \
--active-passive \
--compose-file compose.handoff.yaml \
--coordination-file deploy/coord/active-slot \
--dry-run
imagegen-bridge update docker \
--active-passive \
--compose-file compose.handoff.yaml \
--coordination-file deploy/coord/active-slot \
--yes
The bundled budget allows 31 minutes for an already-dispatched request or
durable worker to drain, three minutes for candidate readiness, and 35 minutes
for held gateway requests. The gateway also enforces an 80 MiB streaming input
limit, a 31-minute forward deadline, and a 60-second response-stream idle
deadline. If configured request limits exceed these values, raise the gateway
hold/forward timeout, slot stop_grace_period, updater readiness timeout, and
request deadline as one coherent inequality: hold > drain + readiness +
margin. The bounded hold prevents a planned handoff from becoming a 502. A
host crash can still leave an already-dispatched remote generation with an
unknown outcome; idempotency safeguards continue to apply.
Health and shutdown¶
GET /health/live is public and content-free for container health checks.
GET /health/ready reads a detail-free cached provider snapshot and verifies
that enabled durable SQLite state is queryable with healthy retention
admission. Probes run on a bounded background cadence; full state remains in
the authenticated GET /v1/diagnostics response.
GET /metrics is enabled in the container profile and protected by the bridge
bearer token. The ordinary Compose profile allows 45 seconds for shutdown; the
handoff profile allows 31 minutes so active synchronous and durable work can
settle before provider shutdown and SIGKILL.
Backups and upgrades¶
Back up /data/state only after stopping the service or by using a
SQLite-consistent snapshot. Artifacts can be backed up independently. Codex
OAuth state is a secret and should use encrypted backup storage. Before an
upgrade, run imagegen-bridge config check with the new binary/image, then
replace the container without deleting named volumes. The configuration loader
rejects unknown or invalid fields before mutating storage.
Troubleshooting¶
If liveness succeeds but readiness returns 503, keep the service on loopback
and run the non-generating diagnostics first:
docker compose exec imagegen-bridge imagegen-bridge \
--config /config/imagegen-bridge.toml doctor --non-interactive --json
docker compose logs --tail 100 imagegen-bridge
An authentication failure normally means /codex-home/auth.json is absent,
expired, or not writable by UID 10001. Re-run codex login outside the
container, copy only the dedicated Codex state described above, preserve its
secret permissions, and restart. Do not print auth.json or mount a whole user
home while debugging.
For permission denied errors, verify ownership of the Codex, state, and
artifact mounts and that the config/workspace mounts are intentionally
read-only. Do not solve this with world-writable permissions. A config failure
should be reproduced with config check; it is non-mutating and reports the
field path without resolving secrets.
If startup reports a SQLite migration error, stop the service, take a
consistent backup of /data/state, and retain the original volume for
diagnosis. Never delete or hand-edit the database as an automatic repair.
Queued jobs resume after a clean restart; a job that was running during an
uncertain shutdown becomes interrupted and is not paid/retried again without
an explicit new request. Provider restart counts, readiness, bounded storage
facts, and redacted recent API events are available from authenticated
GET /v1/diagnostics.
Container verification¶
tests/container-smoke.sh builds the image and runs a disposable fake-Codex
deployment. It verifies non-root UID, read-only rootfs, liveness, provider
readiness, protected metrics/API, backward-compatible version-1 config loading,
graceful SIGTERM exit, persistent SQLite state, and app-server thread resume
after a real container stop/start. The generated fixture is local and it never
performs a live or paid image generation.