# The Twelve-Factor App

> The canonical checklist for an app that is actually shippable and scalable: config in the environment, stateless processes, logs as streams, and more.


---

# The Twelve-Factor App

You shipped something that runs great on your laptop and falls over the moment it meets a second server, a real database, or a deploy at 5pm on a Friday: passwords live in the code, restarting loses data, "works on my machine" has become a personal insult. The Twelve-Factor App is the checklist that turns that fragile thing into something you can deploy, scale, and hand off without dread - and once you internalize it, most of "how do I make this production-ready" stops being a mystery.

## How to read this

This is a field guide, not a spec: each factor exists because of a specific terrible day it prevents, so we lead with the pain and then the rule. You don't have to adopt all twelve at once - read them as moves you reach for when the matching problem shows up. If you only remember three, remember config in the environment, stateless processes, and logs as streams - those three carry most of the weight.

## The phases

1. [One codebase, clean dependencies, config outside the code](01-codebase-deps-config.md) - the foundation that makes a deploy repeatable.
2. [Stateless processes, port binding, and scaling out](02-processes-and-scale.md) - how the running app behaves so you can run many copies.
3. [Dev-prod parity, logs as streams, and the operations factors](03-parity-logs-ops.md) - the factors that keep you sane once it's live.


---

# One codebase, clean dependencies, config outside the code

Picture the most stressful deploy you've lived through. Odds are it traces back to one of three things: nobody was sure which version of the code was running, the new server was missing a library that only lived on the old one, or a password was hard-coded and now it's wrong in the new place. The first three factors exist to make those confusions impossible - get them right and a deploy becomes boring, which is the highest praise infrastructure can earn.

The Twelve-Factor App came out of Heroku, written by people who watched thousands of apps get deployed and noticed the same wounds over and over. It's not a framework you install - it's a set of agreements about how an app relates to its code, its dependencies, and its environment. This phase covers the foundation: the three factors that make a deploy repeatable.

## Factor I - One codebase, many deploys

The terrible day this prevents: two developers each have "the code," they have drifted apart, and nobody can say which one is actually live. Or worse, production is running something that exists in no repository at all because someone SSH'd in and edited a file.

The rule is one-to-one between a codebase and an app, tracked in version control. From that single codebase you produce many **deploys** - staging, production, your laptop, a colleague's review environment. They all run the same code at different versions.

```text
ONE codebase (git repo)
        │
        ├──► deploy: production      (running commit a1b2c3)
        ├──► deploy: staging         (running commit a1b2c3)
        └──► deploy: dev / laptop    (running commit f9e8d7 + edits)
```

*What just happened:* there is exactly one source of truth, and "which version is live" is always answerable - it's a commit hash, not a guess.

If you find yourself copying shared code between two repos, that shared code wants to be a library (a dependency), not a copy-paste. Multiple apps sharing one codebase is a violation too; that's not an app, it's a distributed monolith waiting to surprise you.

## Factor II - Explicitly declare your dependencies

The terrible day: the app runs on your machine, you deploy it, and it crashes because the server doesn't have ImageMagick, or Python 3.12, or that one library you `pip install`-ed by hand eight months ago and forgot about. Your machine has accumulated invisible dependencies. A fresh server has not.

The rule is to declare **every** dependency, exactly, in a manifest the app carries with it - and to isolate so nothing leaks in from the surrounding system.

```bash
# A new teammate clones the repo and runs ONE command.
# Everything the app needs is installed into an isolated environment.

npm ci                 # Node: installs the exact versions from package-lock.json
pip install -r requirements.txt   # Python
bundle install         # Ruby
go mod download        # Go
```

*What just happened:* the manifest plus a lock file pins exact versions, so the install is reproducible. The new teammate's machine now matches yours without anyone reciting setup steps from memory.

The test for whether you've gotten this right: a brand-new developer, or a fresh container, can go from clone to running with one or two documented commands and nothing else. If the real instructions include "oh, you also need to install X globally first," that X is an undeclared dependency. Declare it or vendor it.

> Lock files are not optional clutter. `package-lock.json`, `Pipfile.lock`, `Cargo.lock`, `go.sum` - these pin the *transitive* tree, the dependencies of your dependencies. Without them, "explicitly declared" still drifts.

## Factor III - Store config in the environment

This is the factor people violate most, and pay for most. The terrible day: a password, an API key, or a database URL gets committed to the repo - now it's in git history forever, visible to everyone with read access, and rotating it means a code change and a deploy. Or the gentler-but-still-bad version: production and staging differ only in config, but that config is baked into the code, so you maintain three nearly-identical files and inevitably edit the wrong one.

**Config is everything that varies between deploys.** Database URLs, credentials, the hostname of a backing service, feature flags per environment. It is *not* your routing table or your internal constants - those are the same everywhere, so they belong in the code.

The rule: keep config in **environment variables**, read at runtime, never committed.

```python
# Bad - config baked into the code. Rotating this key means a commit + deploy,
# and the secret is now in git history forever.
DATABASE_URL = "postgres://admin:hunter2@prod-db:5432/app"

# Good - read from the environment. Same code in every deploy;
# only the environment differs.
import os
DATABASE_URL = os.environ["DATABASE_URL"]
```

*What just happened:* the exact same compiled/built artifact now runs in dev, staging, and production. The only thing that changes is the set of environment variables each deploy is handed. Rotating a secret is a config change, not a code change.

A clean litmus test from the original methodology: could you open-source your codebase *right now*, this second, without leaking any credentials? If the answer is no, your config is in the wrong place.

The reason environment variables specifically - rather than a `config.yaml` you forgot to commit, or a "secrets" file - is that they're language-agnostic, OS-standard, and granular per deploy. There's no config file that someone accidentally checks in, no clever framework convention to learn. Every platform on earth knows how to set an env var.

For builders: this is the single highest-leverage factor for a small team. If you do nothing else from this guide, move your secrets out of the code and into the environment. The deeper how-to - `.env` files in dev, secret managers in prod, the precedence rules - lives in /guides/env-vars-and-config.

```quiz
[
  {
    "q": "Under Factor I, what is the correct relationship between a codebase and an app?",
    "choices": ["One codebase can power many apps", "One app has exactly one codebase, with many deploys from it", "Each deploy gets its own codebase", "Apps should share a codebase to reduce duplication"],
    "answer": 1,
    "explain": "One codebase tracked in version control, with many deploys (prod, staging, dev) running it at various versions."
  },
  {
    "q": "What is the litmus test for Factor III (config in the environment)?",
    "choices": ["Does the app start in under one second?", "Could you open-source the codebase right now without leaking any credentials?", "Are all dependencies pinned to exact versions?", "Does staging use a smaller database than production?"],
    "answer": 1,
    "explain": "If open-sourcing the repo would leak secrets, your config (credentials) is wrongly stored inside the code."
  },
  {
    "q": "Why does Factor II insist on a lock file, not only a dependency manifest?",
    "choices": ["Lock files make installs faster", "They pin transitive dependencies to exact versions so installs are reproducible", "They are required by version control", "They encrypt the dependency list"],
    "answer": 1,
    "explain": "A manifest lists what you asked for; the lock file pins the entire transitive tree to exact versions, so every install matches."
  }
]
```


---

# Stateless processes, port binding, and scaling out

The foundation from phase 1 makes a deploy repeatable. This phase is about how the *running* app behaves - the whole point is one word: **multiplicity**, the ability to run many copies of your app, kill any of them, and start new ones, without anyone noticing. Almost every "we can't scale" story is really a story about one of these factors being broken, and once you can run ten copies as safely as one, scaling stops being scary and becomes a slider you drag.

## Factor VI - Processes are stateless

The terrible day: a user uploads a file, the next request lands on a different server, and the file is gone. Or you deploy, the old process dies, and everyone's shopping cart vanishes. Or you can never restart the app during business hours because restarting *loses something*.

The rule: your processes are **stateless and share-nothing**. Anything that must persist goes into a backing service - a database, a cache, object storage. The process itself holds nothing it can't afford to lose the instant it dies.

```text
Request 1 ──► [process A]  writes session to ──► [Redis / Postgres]
Request 2 ──► [process B]  reads  session from ──► [Redis / Postgres]
                  ▲
        either process can serve either request,
        because neither one OWNS the state
```

*What just happened:* because state lives in a shared backing service, it does not matter which process handles a given request. Process B can answer a request that process A started. Kill either one and nothing is lost.

The classic trap is **sticky sessions** - pinning a user to one server so their in-memory session survives. It works until that server restarts or you need to scale, and then it doesn't. The fix is to stop keeping the session in memory: put it in a shared store and any process can serve any user.

Local disk and memory are scratch space only - fine for the duration of one request, gone after. Treat them like a whiteboard you wipe between meetings, never a filing cabinet.

## Factor VII - Export services via port binding

The terrible day is subtler: your app only runs because it's hand-wired into a specific Apache or nginx install on a specific box, with config that lives nowhere in your repo. Move it and it dies, because the webserver *was* part of the app and nobody wrote that down.

The rule: the app is **self-contained** and exports its service by binding to a port. It *is* the web server. It doesn't get injected into one.

```bash
# The app starts its own HTTP listener. Nothing external required to "serve" it.
$ PORT=5000 ./my-app
Listening on http://0.0.0.0:5000

# In another deploy it binds a different port - the env decides (see Factor III).
$ PORT=8080 ./my-app
Listening on http://0.0.0.0:8080
```

*What just happened:* the app owns its own listening socket. A routing layer or load balancer sits in front and forwards traffic to that port, but the app needs nothing injected to be a complete, runnable service. This is exactly why containers work the way they do - a Dockerfile that ends in `EXPOSE 5000` is Factor VII made literal.

## Factor VIII - Scale out via the process model

Now the payoff: because processes are stateless (VI) and self-contained (VII), scaling is no longer "buy a bigger server" - it's "run more processes." This is **scaling out** (more copies) versus **scaling up** (a beefier box), and the process model is what makes scaling out trivial.

The twelve-factor framing is that an app is a collection of **process types**, and you scale each type independently by running more of it:

```text
web    = ./my-app                  → run 8 of these (handle HTTP)
worker = ./my-app --jobs           → run 3 of these (background jobs)
clock  = ./my-app --scheduler      → run 1 of these (periodic tasks)
```

*What just happened:* a traffic spike means running more `web` processes; a backed-up job queue means running more `worker` processes. Each type scales on its own axis, and because every process is stateless, adding or removing one is safe at any moment.

The deep idea: never daemonize your app or write your own PID files to manage copies - let the process model (the operating system, the container orchestrator, the platform) own that. Your job is to make one process that's safe to run N times; the platform's job is to run N of them.

For builders: this trio is the entire reason container platforms and orchestrators exist. Kubernetes "replicas," a `docker compose` `scale`, a platform's "dynos" - all of it assumes your process is stateless, self-contained, and safe to clone, and when scaling that way fights you, the cause is almost always a broken Factor VI. The trade-offs of scaling out at large numbers - coordination, data partitioning, the limits of horizontal scale - are their own topic in /guides/designing-for-scale.

```quiz
[
  {
    "q": "Why do sticky sessions break Factor VI?",
    "choices": ["They use too much memory", "They pin state to one process's memory, so it's lost on restart and blocks scaling", "They require a load balancer", "They store sessions in the database"],
    "answer": 1,
    "explain": "Sticky sessions keep state in one process; when it restarts or you scale, that state is stranded. Put session state in a shared backing service instead."
  },
  {
    "q": "What does 'export services via port binding' (Factor VII) mean?",
    "choices": ["The app must be injected into Apache or nginx", "The app binds its own port and is a self-contained service", "The port number is hard-coded in the source", "Each process exports a different protocol"],
    "answer": 1,
    "explain": "The app is self-contained and serves by binding to a port (often from a PORT env var); a routing layer forwards to it, but nothing is injected into the app."
  },
  {
    "q": "Under the process model (Factor VIII), how do you handle a traffic spike?",
    "choices": ["Move to a bigger server (scale up)", "Run more processes of the web type (scale out)", "Daemonize the app and increase its priority", "Add more local disk to each server"],
    "answer": 1,
    "explain": "You scale out by running more processes of the relevant type. Statelessness makes adding or removing copies safe at any moment."
  }
]
```


---

# Dev-prod parity, logs as streams, and the operations factors

You've made the app repeatable to deploy and safe to clone. The remaining factors are about the part of an app's life that lasts the longest: living in production - they decide whether 2am pages are short or long, whether "it works in staging" means anything, and whether a one-off database fix is routine or terrifying. We'll cover the rest of the twelve here, grouped by the day they save you.

## Factor IV - Backing services are attached resources

The terrible day: your code knows it's talking to *your* Postgres on *that* box, with the connection details woven through the app. The database moves, or you want to swap the local Postgres for a managed one, and you're editing code in twenty places.

The rule: treat every backing service - database, cache, queue, mail server, third-party API - as an **attached resource**, reached only through a URL or credentials in the config. No code-level distinction between a service you run and one a vendor runs.

```text
Swap a backing service with ZERO code change - only config moves:

  DATABASE_URL=postgres://local-db:5432/app     ← dev
  DATABASE_URL=postgres://managed-rds:5432/app  ← prod, different vendor, same code
```

*What just happened:* the app doesn't care who runs the database. Detaching a local one and attaching a managed one is a config change (Factor III again), not a code change. Resources become loosely coupled, swappable, and disposable.

## Factor V - Strictly separate build, release, run

The terrible day: someone fixes a bug by editing code directly on the running server. Now production is a snowflake - the code there exists nowhere else, and the next deploy silently erases the fix.

The rule: three strictly separate stages, and you can never edit code at run time.

```text
BUILD   ── compile code + fetch deps ──►  an immutable build artifact
RELEASE ── build  +  config ──────────►  a release, tagged & unique (v347)
RUN     ── execute that release ──────►  the process running in production
```

*What just happened:* each release is immutable and uniquely tagged, so you always know exactly what's running, and rollback is "run the previous release" rather than "reverse-engineer what changed." Code cannot be modified at run time, which kills the snowflake server for good.

## Factor IX - Disposability: fast startup, graceful shutdown

The terrible day: a deploy takes ten minutes per process to start, so scaling up during a spike arrives long after the spike is over. Or a shutdown kills a process mid-request and the user gets a broken response.

The rule: processes are **disposable** - they start fast and shut down gracefully. On `SIGTERM`, stop accepting new work, finish what's in flight, and exit. Workers should return unfinished jobs to the queue so another process can pick them up.

```bash
# Platform sends SIGTERM. A well-behaved process:
#   1. stops accepting new requests/jobs
#   2. finishes in-flight work (within a grace period)
#   3. exits cleanly
$ kill -TERM <pid>
[info] draining: 2 requests in flight
[info] drained, exiting 0
```

*What just happened:* shutdown lost no work and corrupted nothing, so the platform can stop and start your processes freely. Fast startup plus graceful shutdown is what makes the scaling from phase 2 actually responsive and safe.

## Factor X - Dev-prod parity

The terrible day: "it works on my machine" - and it really did, because your machine runs SQLite and a different OS while production runs Postgres on Linux. The gap between dev and prod is where bugs hide until the worst possible moment.

The rule: keep dev and prod as similar as you can across three gaps - **time** (deploy hours after writing, not weeks), **personnel** (the people who write code also deploy it), and **tools** (same backing services in dev as in prod; don't substitute SQLite for Postgres). Containers exist in large part to close the tools gap.

## Factor XI - Logs are event streams

The terrible day: you need to know what happened during an incident, but the logs are scattered across files on twelve servers, some rotated away, and you're SSH-ing box to box grepping by hand at 3am.

The rule: your app does not manage log files. It treats logs as a **stream of events written to stdout**, unbuffered, and stays completely ignorant of where they go. The execution environment captures the stream and routes it - to the terminal in dev, to an aggregator in prod.

```text
app  ──►  writes events to stdout  ──►  (app's job ends here)
                                          │
              environment routes the stream ▼
        dev: your terminal      prod: aggregator → search, alerts, retention
```

*What just happened:* the app got simpler - no log rotation, no file paths, no log config per environment - and prod got more powerful, because the captured stream can be searched, alerted on, and retained centrally across every process at once.

## Factor XII - Run admin tasks as one-off processes

The terrible day: you run a database migration or a data-fix script by hand on your laptop, against production, with whatever version of the code happened to be checked out. It half-works, and now prod data is in a state nobody designed.

The rule: run admin and management tasks - migrations, one-off scripts, a REPL console - as **one-off processes in an identical environment to the long-running ones**, against the same release, the same config, the same dependencies.

```bash
# Same release, same env, same deps - just a different command, run once.
$ heroku run rails db:migrate           # or:
$ kubectl exec deploy/app -- ./manage migrate
```

*What just happened:* the one-off task ran with the exact code and config of the live release, so it can't drift from production. Admin work stops being a risky improvisation and becomes a repeatable, trustworthy operation.

## The whole checklist, in one breath

When something feels un-shippable, walk the list and find the broken factor: one codebase, declared dependencies, config in the environment, backing services as attached resources, separated build-release-run, stateless processes, port binding, scale via the process model, disposability, dev-prod parity, logs as streams, admin tasks as one-off processes. Most "how do I make this production-ready" questions are one of these twelve in disguise.

For builders: you don't need a giant platform to honor these. A single small service with secrets in env vars, a lock file, stdout logging, and a graceful `SIGTERM` handler is already most of the way there - and it'll survive contact with a real deploy.

```quiz
[
  {
    "q": "According to Factor XI, what should an app do with its logs?",
    "choices": ["Rotate them into dated files on local disk", "Write them as an unbuffered event stream to stdout and let the environment route them", "Ship them directly to a database the app manages", "Email them to the on-call engineer"],
    "answer": 1,
    "explain": "The app writes events to stdout and stays ignorant of routing; the execution environment captures and routes the stream."
  },
  {
    "q": "Which describes a one-off admin process done right (Factor XII)?",
    "choices": ["A migration run from a developer's laptop against prod", "A script run in an environment identical to the app's, against the same release and config", "A long-running daemon that polls for admin commands", "Editing the database by hand over SSH"],
    "answer": 1,
    "explain": "Admin tasks run as one-off processes in an environment identical to the running app - same release, config, and dependencies - so they can't drift from production."
  },
  {
    "q": "What does a disposable process (Factor IX) do when it receives SIGTERM?",
    "choices": ["Immediately exits, dropping any in-flight work", "Ignores it and keeps running until killed", "Stops taking new work, finishes in-flight work, then exits cleanly", "Restarts itself in place"],
    "answer": 2,
    "explain": "Graceful shutdown means refusing new work, draining what's in flight, and exiting cleanly - workers return unfinished jobs to the queue."
  }
]
```
