Twelve-Factor Apps skill for Codex
One build, any environment, kill it anytime. Codex audits each of the twelve factors with concrete repository probes and records evidence by file and line instead of a generic checklist. It moves environment values into validated config, adds graceful SIGTERM handling that survives Kubernetes rollouts, sends logs to stdout and runs migrations as a separate release step, then proves it by running one image against two environments.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- twelve-factor-apps Installs to ~/.agents/skills/twelve-factor-apps/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install twelve-factor-apps The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project twelve-factor-apps Installs to .agents/skills/twelve-factor-apps/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/twelve-factor-apps.zipunzip -o twelve-factor-apps.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) twelve-factor-apps PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $twelve-factor-apps, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Audits and fixes an application against the twelve-factor methodology for containers, Kubernetes and PaaS - config in environment variables, stateless processes, backing services as attached resources, build/release/run separation, port binding, graceful SIGTERM shutdown, logs to stdout, dev/prod parity and one-off admin processes - with grep-based checks and code fixes. Use when preparing an app for containers or a PaaS, when pods lose data on restart, deploys drop requests, config is hard-coded per environment, or someone asks for a twelve-factor review.
Use it when
- An app is moving to containers, Kubernetes or a PaaS
- Config differs per environment through code branches or rebuilt images
- Rolling deploys drop requests or pods lose data on restart
- You want an evidence-based readiness checklist for a service
Use something else when
- You are writing or slimming the Dockerfile itself: use dockerfile-best-practices
- The task is storing and rotating secrets: use secrets-management
How to check the result
The same image starts against two different env files without a rebuild, and a SIGTERM during a slow request lets it finish and exit 0 within the grace period.
The mistake it prevents
Using a shell-form CMD or npm start as the container entrypoint, so SIGTERM never reaches the app and every deploy kills in-flight requests.
Done well means the same immutable build artifact runs in every environment with only environment-provided config changing, any instance can be killed at any moment without losing data or dropping in-flight requests, and the app scales by adding processes. The twelve factors are a checklist toward that, not a religion; note justified exceptions.
Before auditing
- Read
AGENTS.md, the Dockerfile, compose files, Kubernetes manifests or Helm charts, Procfile, and CI deploy jobs. These show how the app actually runs. - Find config loading (
settings.py,config/*.yml,appsettings.*.json,.envhandling) and the process entry point (signal handling, server start). - Note the platform: Kubernetes, ECS, Cloud Run, Heroku-style PaaS, VMs. Some fixes (graceful shutdown timing, health probes) depend on it.
Audit checklist with evidence
Run the probes, record pass, fail or exception with file paths.
| # | Factor | Pass when | Probe |
|---|---|---|---|
| 1 | Codebase | One repo (or monorepo package) per deployable; many deploys of it | Check for copy-pasted forks per customer or environment |
| 2 | Dependencies | All declared and locked; nothing relies on host tools | Lockfile present and used in build; probe A for implicit host tools |
| 3 | Config | Environment-specific values come from env or a secret store, not code | Probe B; look for if ENV == "production" branches |
| 4 | Backing services | DB, cache, queue, SMTP reached by URL/credentials from config; swappable | Hard-coded hostnames or local-socket assumptions |
| 5 | Build, release, run | Build once; release = build + config; no code changes at runtime | Images rebuilt per environment, migrations or asset builds at container start |
| 6 | Processes | Stateless; no sticky sessions; local disk only for scratch | Probe C; in-memory session stores |
| 7 | Port binding | App serves HTTP itself on $PORT | Requires an external app server injected at deploy |
| 8 | Concurrency | Scale by process type (web, worker, scheduler) | One process doing web plus cron plus queue work |
| 9 | Disposability | Fast start, graceful SIGTERM, idempotent jobs | Signal handlers; startup time; shutdown test below |
| 10 | Dev/prod parity | Same backing service types and versions in dev and prod | SQLite in dev with Postgres in prod; compose image tags vs prod versions |
| 11 | Logs | Unbuffered event stream to stdout/stderr; no log files or rotation in the app | Probe D |
| 12 | Admin processes | Migrations and one-off tasks run as separate processes from the same release | Migration scripts run by hand from laptops or baked into web startup |
rg -n "apt-get install|brew install" scripts/ Makefile # A: implicit host tools
rg -n "localhost|127\.0\.0\.1|\.internal|prod|staging" src -g '!**/*test*' # B: env-specific values in code
rg -n "writeFile|/tmp/|uploads/|open\(.*['\"]w" src # C: local disk state
rg -n "FileHandler|RotatingFileHandler|transports\.File|\.log['\"]" src # D: log files
Add the modern extras every reviewer asks about: health endpoints (separate liveness and readiness), secrets from a secret manager rather than plain env in manifests, telemetry (metrics and traces), and a non-root container user.
Fixes that come up most
Config
# settings.py: fail fast on missing config; no per-environment branches
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
redis_url: str
log_level: str = "INFO"
feature_x_enabled: bool = False
settings = Settings() # reads env vars; raises at startup if DATABASE_URL is missing
Keep a committed .env.example with every key and no real values. Never commit .env.
Config is what varies between deploys; constants that never vary (page size limits,
route tables) stay in code.
Graceful shutdown
The orchestrator sends SIGTERM, waits a grace period (Kubernetes default 30 s), then SIGKILLs. On SIGTERM: fail the readiness check, stop accepting new connections, finish in-flight requests, close pools, exit 0.
// Node HTTP server
const server = app.listen(process.env.PORT ?? 3000);
process.on("SIGTERM", () => {
ready = false; // readiness endpoint now returns 503
server.close(async () => { // stops accepting, waits for in-flight
await db.end();
process.exit(0);
});
setTimeout(() => process.exit(1), 25_000).unref(); // stay under the grace period
});
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt)
defer stop()
go func() { _ = srv.ListenAndServe() }()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)
Container gotcha: a shell-form CMD npm start makes sh PID 1, which does not forward
SIGTERM. Use exec form (CMD ["node", "server.js"]) or an init such as tini, and
avoid npm start as the entrypoint in production.
Kubernetes gotcha: endpoints are removed asynchronously, so new requests can arrive
after SIGTERM. A short preStop sleep (5 to 10 s) or keeping the listener open briefly
avoids dropped requests during rollouts.
Logs
Log one JSON object per line to stdout; let the platform ship and rotate. In Python, set
PYTHONUNBUFFERED=1 in the image or logs arrive late and out of order.
Admin processes and migrations
Run migrations as a separate release step (Kubernetes Job, Helm hook, PaaS release phase), not in every web container’s startup, where N replicas race to migrate. Running migrations or one-off scripts against production is an outward-facing, potentially destructive action: confirm with the user before executing any of them.
Justified exceptions
- Large or structured config (routing tables, feature matrices) can live in mounted files from a ConfigMap or config service; the principle is “outside the build”, not “only env”.
- Secrets in plain environment variables leak into crash dumps and child processes; mounting them from a secret manager is better than the original factor.
- Stateful services (databases, brokers) are not twelve-factor apps; do not force them.
Verification
- Build the image once and run it with two different env files; it must start and point at different backing services with no rebuild.
- Shutdown test: start the app, send a slow request, send SIGTERM
(
docker kill --signal=SIGTERM <id>), confirm the request completes and the process exits 0 within the grace period. - Kill a running instance under light load (
heyork6against a local or staging copy) and confirm zero failed requests and no lost uploads or sessions. rgprobes from the checklist come back clean or each hit is documented as acceptable.
Output template
## Twelve-factor review: <app>
| # | Factor | Status | Evidence | Fix |
|---|---|---|---|---|
| 3 | Config | Fail | `src/db.ts:12` hard-codes `prod-db.internal` | Read `DATABASE_URL`; added to `.env.example` |
| 9 | Disposability | Fail | shell-form CMD, no SIGTERM handler | Exec-form CMD, graceful shutdown in `server.ts` |
**Exceptions accepted:** <factor, reason>
**Verification run:** <tests and results> What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Twelve-Factor Apps"
short_description: "One build, any environment, kill it anytime"
default_prompt: "$twelve-factor-apps Do a twelve-factor review of this service before we move it to Kubernetes" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.