---
url: /entries/twelve-factor-app.md
description: >-
  The Twelve-Factor App methodology for building portable, scalable
  software-as-a-service.
---

# Twelve-Factor App

## Quick Reference

1. **Codebase** One codebase, many deploys
2. **Dependencies** Explicitly declare and isolate dependencies
3. **Config** Store config in the environment
4. **Backing Services** Treat backing services as attached resources
5. **Build, Release, Run** Strictly separate build and run stages
6. **Processes** Execute the app as one or more stateless processes
7. **Port Binding** Export services via port binding
8. **Concurrency** Scale out via the process model
9. **Disposability** Fast startup and graceful shutdown
10. **Dev/Prod Parity** Keep environments as similar as possible
11. **Logs** Treat logs as event streams
12. **Admin Processes** Run admin tasks as one-off processes

## 1 - Codebase

One repo per app. One-to-many relationship: one codebase → many deploys (prod, staging, local).

* Multiple apps sharing code → extract a shared library, don't share a repo
* Multiple codebases in one repo → that's a distributed system, not one app
* One repo can have multiple deployables, if those deployables still build out the same app

```
repo/
└── myapp/          # single codebase
    ├── deploy/prod
    ├── deploy/staging
    └── deploy/dev
```

## 2 - Dependencies

Declare all dependencies explicitly in a manifest. Never rely on implicit system-wide packages.

| Language | Manifest                              | Isolation tool          |
| -------- | ------------------------------------- | ----------------------- |
| Python   | `requirements.txt` / `pyproject.toml` | `venv`, `poetry`        |
| Node.js  | `package.json`                        | `node_modules` (local)  |
| Ruby     | `Gemfile`                             | `bundler`               |
| Go       | `go.mod`                              | module cache            |
| Java     | `pom.xml` / `build.gradle`            | Maven/Gradle local repo |

* System tools your app shells out to (e.g. `curl`, `ImageMagick`) should also be declared — vendored or ensured via a
  build step
* A clean checkout + dependency install must be sufficient to run the app

## 3 - Config

Everything that varies between deploys (dev, staging, prod) belongs in environment variables — never in code or
checked-in config files.

**Fails the test:**

```dotenv
# hardcoded — violates factor III
DB_URL = "postgres://user:pass@prod-db.internal/myapp"
```

**Passes the test:**

```python
import os
DB_URL = os.environ["DATABASE_URL"]
```

* `.env` files are fine locally; use your platform's secret management in production (AWS SSM, Vault, Doppler, etc.)
* Do **not** group config into named environments (`config/production.py`) — this doesn't scale as environments multiply
* Config that doesn't change between deploys (e.g. internal routing constants) can live in code

## 4 - Backing Services

Treat databases, caches, queues, and external APIs as *attached resources* — swappable via config with no code changes.

```dotenv
# Both are "databases" to the app — only the URL changes
DATABASE_URL="postgres://localhost/dev"
DATABASE_URL="postgres://user:pass@rds.amazonaws.com/prod"

# Same for third-party services
SMTP_URL="smtp://localhost:1025"          # local mailhog
SMTP_URL="smtp://user:pass@smtp.sendgrid.net:587"
```

Local and third-party services are treated identically. A production DB can be swapped for a replica with no code
changes.

## 5 - Build, Release, Run

Strictly separate the three stages:

```
Source code
    │
    ▼
[BUILD]    Compile, fetch deps, build assets → immutable build artifact
    │
    ▼
[RELEASE]  Build artifact + config → versioned, immutable release
    │
    ▼
[RUN]      Execute release in environment (process manager, container runtime)
```

* Releases are **immutable** — never modify a running release; create a new one
* Every release should have a unique ID (timestamp or incrementing number)
* Rollback = activate a previous release

This maps directly to Docker: `docker build` → tag+push → `docker run`.

## 6 - Processes

The app runs as one or more **stateless, share-nothing** processes.

* Any data that needs to persist must go to a backing service (DB, cache, object store)
* Never assume in-memory state or local disk is available on the next request
* Sticky sessions violate this factor — use a shared session store (Redis, etc.) instead

```python
# Bad: local disk as durable storage
open("/var/data/uploads/file.jpg", "wb").write(data)

# Good: upload to object store
s3.upload_fileobj(data, bucket, key)
```

## 7 - Port Binding

The app is self-contained and exports HTTP (or other services) by binding to a port — no external web server required.

```bash
# The app binds its own port
uvicorn main:app --host 0.0.0.0 --port $PORT   # Python/FastAPI
node server.js                                  # Node (listens on process.env.PORT)
```

* In production, a routing layer (load balancer, reverse proxy) sits in front and forwards traffic
* This also means one app can become a backing service for another — just point to its URL

## 8 - Concurrency

Scale horizontally by adding more processes, not by making processes bigger.

```
web process:    handles HTTP requests     → scale out for traffic
worker process: handles background jobs   → scale out for queue depth
clock process:  handles scheduled tasks   → typically single instance
```

* Design your process types (web, worker, etc.) to be independently scalable
* Avoid writing daemons or PID files — let the process manager (systemd, Docker, ECS) handle lifecycle
* Pairs with Factor VI: stateless processes are trivially horizontally scalable

## 9 - Disposability

Processes should start fast and shut down gracefully. Treat them as cattle, not pets.

**Startup:** aim for seconds, not minutes. Enables fast scaling and deploys.

**Shutdown (SIGTERM):**

```python
import signal, sys

def handle_sigterm(sig, frame):
    # finish current request, reject new ones
    server.shutdown()
    sys.exit(0)

signal.signal(signal.SIGTERM, handle_sigterm)
```

**For workers:** return in-progress jobs to the queue before exiting so another worker can pick them up.

* Crash-only design: a process that crashes should be safe to restart at any time
* Use idempotent job processing to handle unexpected restarts mid-task

## 10 - Dev/Prod Parity

Minimize the gap between development and production across three dimensions:

| Gap        | Anti-pattern                     | Fix                             |
| ---------- | -------------------------------- | ------------------------------- |
| **Time**   | Deploy once a week               | Deploy frequently (CI/CD)       |
| **People** | Devs write code, ops deploy it   | Devs own deployments            |
| **Tools**  | SQLite locally, Postgres in prod | Same backing service everywhere |

Use Docker Compose or similar to run prod-equivalent services locally:

```yaml
# docker-compose.yml
services:
  db:
    image: postgres:16 # same version as prod
  cache:
    image: redis:7
```

Subtle bugs caused by SQLite↔Postgres or memcache↔Redis differences are common and entirely avoidable.

## 11 - Logs

Treat logs as a continuous stream of time-ordered events. The app itself has no concern for routing or storing them.

```python
# Good: write to stdout, let the environment handle it
import sys
print(json.dumps({"level": "info", "msg": "request received", "path": "/api/users"}), file=sys.stdout)

# Bad: app manages its own log files
logging.FileHandler("/var/log/myapp/app.log")
```

* In development: stream to terminal
* In production: captured by the execution environment (e.g. Docker, ECS) and routed to a log aggregator (CloudWatch,
  New Relic, etc.)
* Structured JSON logs make querying and alerting much easier

## 12 - Admin Processes

Run one-off admin tasks (migrations, scripts, console sessions) as isolated processes using the same codebase and config
as the app.

```bash
# Django migration — same container image, same env vars
docker run --env-file .env myapp:v42 python manage.py migrate

# Rails console
heroku run rails console --app my-production-app

# One-off data fix script
kubectl exec -it deploy/myapp -- python scripts/backfill_user_ids.py
```

* These should ship in the same release as the app code — not a separate repo or manual procedure
* Never run admin tasks against production via a local checkout with local config

***

## Common Violations Cheat Sheet

| Symptom                                                 | Violated factor                 |
| ------------------------------------------------------- | ------------------------------- |
| `API_KEY=abc123` committed to git                       | 3 — Config                      |
| Works on my machine, broken in prod                     | 10 — Dev/Prod Parity            |
| Uploaded files vanish after redeploy                    | 6 — Processes                   |
| Can't add a second web server instance                  | 6 / 8 — Processes / Concurrency |
| DB credentials differ in code vs env                    | 3 — Config                      |
| App writes logs to `/var/log/app.log`                   | 11 — Logs                       |
| `db.sqlite3` used locally, Postgres in prod             | 10 — Dev/Prod Parity            |
| Migration steps documented in Confluence, not automated | 12 — Admin Processes            |
