Guide

Twelve-factor app explained

A team ships a Node API that works on a developer laptop but breaks in staging because someone edited config.prod.json by hand. Sessions stick to server 2 after a deploy, so users get logged out randomly. Logs live in /var/log/app.log on a VM nobody can SSH into anymore. These are not framework bugs — they are deployment architecture failures. The twelve-factor app methodology, published by Heroku engineers in 2011, codifies how to build software-as-a-service apps that run identically in development and production, scale horizontally without surgery, and survive platform changes from bare metal to Kubernetes. This guide walks through all twelve factors, how they interact with containers and CI/CD, a SaaS refactor worked example, a decision table for when to bend the rules, common pitfalls, and a production checklist.

What twelve-factor solves

Before cloud platforms, applications were installed like desktop software: operators copied files, edited local config, and prayed upgrades were reversible. SaaS and PaaS changed the contract — you deploy many times per day to disposable instances behind a load balancer. Twelve-factor is a checklist for that world:

  • Portability — run on Heroku, AWS ECS, Fly.io, or your own k8s cluster without rewriting deploy scripts.
  • Scalability — add capacity by starting more processes, not by buying a bigger box.
  • Disposability — kill any instance at any time; the app keeps working.
  • Dev/prod parity — bugs found locally reproduce in production because the stack matches.

The factors are not a religion. A batch ETL job or embedded firmware project will violate several by design. But for HTTP APIs, webhooks, and background workers in the cloud, they remain the most cited baseline for microservice hygiene.

Factors I–IV: codebase, dependencies, config, backing services

I. Codebase — one app, many deploys

Track exactly one repository per deployable application. Multiple apps sharing a repo is acceptable only if they are versioned and deployed together (e.g. a monorepo with independent release tags per service). Use branches for features, not separate repos for staging vs production code. Deployments are immutable snapshots of a commit — never hot-patch servers.

II. Dependencies — explicit and isolated

Declare dependencies in manifest files (package.json, requirements.txt, go.mod) and never rely on system-wide packages. Reproducible installs via lockfiles (package-lock.json, poetry.lock) plus container images or virtual environments ensure staging matches production byte-for-byte.

III. Config — store in the environment

Configuration that varies between deploys — database URLs, API keys, feature flags — belongs in environment variables, not in source code or checked-in YAML per environment. This enables the same artifact to promote from CI through staging to production with only env changes. Use secrets managers (Vault, AWS Secrets Manager, Doppler) to inject values at runtime rather than baking them into images.

IV. Backing services — treat as attached resources

Databases, queues, caches, and object storage are attached resources accessed via URLs/credentials in config. Swap a local Postgres for RDS by changing DATABASE_URL — no code changes. The app should not care whether Redis runs on localhost or ElastiCache; both are backing services bound through config.

Factors V–VIII: build, processes, port binding, concurrency

V. Build, release, run — strict separation

The lifecycle has three stages:

  1. Build — compile assets, bundle JS, run tests; output is an immutable build.
  2. Release — combine build + config (env) into a release ready to run; assign a unique release ID.
  3. Run — execute release processes in the target environment.

Never edit running containers. Roll forward by deploying a new release; roll back by re-running a previous release ID. CI/CD pipelines should encode this split explicitly — build artifacts in artifact storage, promote releases via deploy hooks.

VI. Processes — stateless and share-nothing

Application processes should be stateless. Session data, uploaded files, and job queues belong in backing services (Redis, S3, Postgres), not in local disk or RAM that disappears when the process dies. Sticky sessions are a smell — fix the architecture instead of pinning users to servers.

VII. Port binding — export services via port

The app is self-contained: it binds to a port and serves HTTP (or gRPC) directly. Do not assume Apache or nginx must be co-installed on the same machine — though in production you often place a reverse proxy in front for TLS termination and routing. The factor means your service is a first-class network citizen, not a CGI script dropped into someone else's web root.

VIII. Concurrency — scale out via the process model

Scale by running more processes, not bigger threads inside one giant JVM. Use a process manager (systemd, Kubernetes replicas, Heroku dynos) to map workload types to process counts: web workers handle HTTP, worker processes drain queues. CPU-bound and I/O-bound pools can be sized independently.

Factors IX–XII: disposability, parity, logs, admin processes

IX. Disposability — fast startup and graceful shutdown

Processes start quickly (seconds, not minutes) so autoscaling reacts to traffic spikes. They shut down gracefully on SIGTERM: finish in-flight requests, stop accepting new work, then exit. Pair with connection draining and health probes so orchestrators do not kill mid-request. Crash-only design (restart on failure) is fine when startup is cheap and idempotent.

X. Dev/prod parity — keep environments similar

Minimize gaps between development, staging, and production:

  • Time gap — deploy hours after merge, not months.
  • Personnel gap — developers participate in on-call; ops is not a separate priesthood.
  • Tools gap — use the same backing services locally (Docker Compose) that production uses, not SQLite in dev and Postgres in prod.

Perfect parity is expensive; aim for structural parity (same databases, same message broker) even if scale differs.

XI. Logs — treat as event streams

Apps write logs to stdout/stderr as unbuffered event streams. The execution environment aggregates, routes, and stores them (CloudWatch, Loki, Datadog). Apps never manage log rotation files. Use structured JSON logs with correlation IDs so distributed traces stitch together.

XII. Admin processes — run as one-off tasks

Database migrations, cache warms, and REPL consoles run as one-off processes in the same release environment as the app — same code, same config, same dependencies. In Kubernetes this is a Job; on Heroku, heroku run. Never SSH to production and run ad-hoc scripts from a random laptop.

Worked example: refactoring a legacy SaaS API

Imagine InvoiceFlow, a billing API still deployed as a tarball on two VMs with nginx, PHP sessions in /tmp, and database credentials in config.php. Traffic doubled; deploys take 45 minutes and require downtime. A twelve-factor refactor plan:

  1. Extract config — move DB host, Stripe keys, and S3 bucket to env vars; delete per-environment PHP files from git.
  2. Externalize sessions and uploads — Redis for sessions, S3 for PDF invoices; VMs become interchangeable.
  3. Containerize — Dockerfile with composer install --no-dev; image runs php -S 0.0.0.0:8080 or php-fpm behind an ingress.
  4. CI pipeline — on merge: build image, tag with git SHA, push to registry, deploy release invoiceflow-abc123 to staging, run migration Job, smoke test, promote to prod with rolling update.
  5. Scale web and workers separately — 4 web replicas behind load balancer; 2 queue workers consuming Stripe webhook backlog.
  6. Logging — JSON lines to stdout; Loki dashboards replace SSH tailing.

After migration, a deploy is git push plus a 3-minute rolling restart. No sticky sessions. Rollback is redeploying the previous image tag. The refactor did not require rewriting business logic — only how the app is packaged and run.

Decision table: when to follow, bend, or break

Scenario Recommendation Rationale
Stateless REST/GraphQL API Follow all twelve strictly Horizontal scale and zero-downtime deploys depend on it
WebSocket game server with in-memory room state Bend VI (stateless) — use Redis pub/sub or dedicated shard routing Real-time state is inherent; isolate and replicate explicitly
Serverless functions (Lambda) Bend VII/VIII — platform binds ports and scales for you Factors still apply to config, dependencies, and logs
Monorepo with 8 microservices Bend I — one repo, independent deploy tags per service Codebase factor allows shared libraries with separate releases
Edge CDN static site Break several — no processes, no port binding Twelve-factor targets long-running server apps, not static assets
GPU batch inference job Bend IX — slow cold starts acceptable if queued Disposability target is minutes, not milliseconds

Mapping factors to modern tooling

  • Docker — packages II (dependencies), V (build artifact), VII (port expose).
  • Kubernetes — VIII (replicas), IX (probes + preStop hooks), XII (Jobs/CronJobs).
  • Terraform / IaC — III + IV wire backing services and inject config at deploy time.
  • GitHub Actions / GitLab CI — V encodes build-release-run; XI ships logs to aggregators.
  • Feature flags (LaunchDarkly, Unleash) — extend III for runtime toggles without redeploy.

Twelve-factor predates Kubernetes but aligns naturally — pods are disposable processes, ConfigMaps/Secrets are env config, Ingress is port binding at the edge.

Common pitfalls

  • Secrets in git — even “private” repos leak; use env injection only.
  • Local filesystem as database — uploaded avatars on disk break when replicas > 1.
  • Long-running deploy scripts that mutate servers — violates build-release-run; no reproducible rollback.
  • Different dependency versions in CI vs prod — “works on my machine” from skipping lockfiles.
  • Logging to files inside containers — logs vanish when the pod is garbage-collected.
  • Running migrations manually in prod — untracked schema drift; automate as release step XII.
  • Ignoring graceful shutdown — load balancer keeps sending traffic to dying instances; users see 502s during deploys.

Production checklist

  • Single deployable artifact per service, tagged with immutable version (git SHA).
  • All environment-specific values in env vars or secrets manager — zero in repo.
  • Lockfiles committed; CI installs with --frozen or equivalent.
  • Session and cache state in Redis or similar — not local memory.
  • Health endpoints for liveness and readiness; SIGTERM handler drains connections.
  • Structured logs to stdout with request IDs; no file-based logging in containers.
  • Migrations run as one-off Jobs before traffic shifts to new release.
  • Staging uses same backing service types as production.
  • Rollback procedure documented: redeploy previous release ID in under 5 minutes.
  • Load test validates horizontal scale (N replicas behave like 1 logical app).

Key takeaways

  • Twelve-factor is a methodology for SaaS apps that deploy frequently to disposable infrastructure.
  • Config in the environment and stateless processes are the two factors teams violate most — and pay for during scale events.
  • Build-release-run separation makes rollbacks boring instead of heroic.
  • Containers and Kubernetes implement the process model; they do not replace thinking about backing services and parity.
  • Bend factors deliberately for real-time state or serverless — but document why, do not drift accidentally.

Related reading