The 12-Factor App, The Rulebook Behind Every Cloud-Native Deploy
Written by Heroku engineers in 2011, still quoted in every senior interview in 2026. Here is what each of the twelve factors actually means, why Kubernetes was built around them, and how to apply them to a real Deployment manifest.
The app that runs fine on your laptop and dies in production
You build an app. It runs perfectly on your machine. You deploy it to a server and it half-works, until you add a second server and sessions start vanishing, or you need to change the database URL and realize it's hardcoded in three files, or your logs are scattered across machines you can't even reach. None of this is a bug in your code. It's a set of habits your code never had to learn while it lived alone on your laptop.
In 2011, engineers at Heroku had watched thousands of apps make exactly these mistakes on their platform. So they wrote down the twelve habits that separated the apps that scaled cleanly from the ones that fought the platform at every turn. They called it the Twelve-Factor App. Fifteen years later it is still the single best checklist for "is this app actually cloud-ready?", and, not coincidentally, it is the mental model Kubernetes was designed around.
Who this is for
Engineers who can deploy an app but have been burned by it behaving differently in production, vanishing sessions, hardcoded config, logs you can't find. No prior Kubernetes knowledge needed; we build up to a real manifest. If you've ever asked "why does Kubernetes work this way?", this is the answer underneath it.
What "twelve-factor" actually means
A twelve-factor app is one built so that any copy of it can run, scale, and be replaced on any machine, with nothing it needs stored inside itself, everything it depends on is handed to it from the outside.
That single property, self-contained code, everything-else from outside, is what every individual factor is really protecting. Config comes from outside. Backing services come from outside. The decision of how many copies to run comes from outside. The app itself becomes a small, predictable, disposable unit. And a disposable unit is exactly what a cloud platform can move, restart, and multiply at will.
A rental car: any one in the lot will do, you bring nothing into it, and if it breaks you swap it for anotherA stateless process: any instance serves any request, config is injected, and a crashed one is replaced instantly
You never store your luggage permanently in a rental, you'd lose it on swapNever store session state on local disk, it vanishes when the instance is replaced
The rental company hands you the keys and the fuel card at pickupThe platform injects config and credentials as environment variables at startup
Every car in the fleet is the identical model, so the experience is the sameOne immutable image runs in every environment, dev, staging, and prod are byte-for-byte the same build
A twelve-factor app behaves like a rental car, not the car in your garage.
The picture: how a twelve-factor app gets to production
Before the factors one by one, look at the shape of the whole flow. A twelve-factor app moves from a single Git repo, through a build that produces one immutable image, onto a platform that injects config and runs many identical stateless copies, each streaming its logs out to be collected elsewhere. Almost every factor is just a rule about one arrow in this picture.
One codebase → one build → one immutable image → config injected from the environment → many stateless processes on a platform → logs streamed to stdout for collection.
1
One codebase, tracked in Git
Factor I. A single repo per app, deployed to many environments. Not one repo per environment, not many apps sharing one repo, one app, one codebase, many deploys.
2
CI builds one immutable image
Factor V (build/release/run, kept separate). The build stage turns code into a single artifact, a container image tagged with the commit SHA. After this point nothing in the image changes.
3
The platform injects config
Factor III. Database URLs, API keys, and feature flags are handed to the process as environment variables at launch. The same image gets different values in staging vs prod.
4
It runs as many stateless copies
Factors VI and VIII. The platform starts N identical replicas. Any one can serve any request because none of them holds state locally. Need more throughput? Start more copies.
5
Logs stream out to stdout
Factor XI. Each process writes events to stdout as a stream and forgets them. The platform captures and routes that stream, the app never manages log files.
All twelve factors, mapped to Kubernetes
Here is the heart of it: the original twelve factors, what each one actually asks of you, and the concrete Kubernetes mechanism that enforces it. This is also the answer to "why is Kubernetes shaped like this?", almost every primitive in the table exists to make one factor automatic.
Factor
What it means
Kubernetes mechanism
I. Codebase
One repo per app, tracked in version control, deployed many times
One Git repo → one image per service; GitOps reconciles the cluster to the repo
II. Dependencies
Declare every dependency explicitly; never rely on what's on the host
Dependencies baked into the image via the `Dockerfile`; the host stays irrelevant
III. Config
Config lives in the environment, never in code
`ConfigMap` (non-secret) and `Secret` (encrypted), injected as env vars
IV. Backing services
Treat databases, caches, queues as attached resources swapped via a URL
Connection strings in a `Secret`; a `Service` / external endpoint you point at
V. Build, release, run
Keep the three stages strictly separate
CI builds the image; a `Deployment` spec is the release; the kubelet runs it
VI. Processes
Run the app as stateless, share-nothing processes
Stateless `Pod`s; persistent state pushed out to a database or `PersistentVolume`
VII. Port binding
Be self-contained, export your service by binding to a port
Container `containerPort`; a `Service` routes traffic to it
VIII. Concurrency
Scale out by running more processes, not bigger ones
`replicas` count and the `HorizontalPodAutoscaler`
Keep dev, staging, and prod as similar as possible
The same image in every environment; only injected config differs
XI. Logs
Treat logs as event streams to stdout, don't manage files
Write to stdout/stderr; the node agent (e.g. Fluent Bit) collects and ships them
XII. Admin processes
Run one-off admin tasks as separate, identical processes
`Job` / `kubectl exec` using the same image, not a special build
The twelve factors and how each maps onto a Kubernetes deployment.
Pro tip
If you can only keep three for an interview, keep **III Config (env vars)**, **VI Processes (stateless)**, and **XI Logs (stdout)**. These three drive most of how Kubernetes behaves and come up in the large majority of system-design conversations.
The three factors that bite people most
Factor III, Config in the environment
Never hardcode a database URL, API key, or feature flag. The litmus test: could you open-source your repo right now without leaking a secret or breaking an environment? If a production password lives in a source file, your config is in the wrong place. Read everything from the environment so the same code runs everywhere and only the injected values change. There's a full treatment in Environments & Config: Dev / Staging / Prod Done Right.
Factor VI, Stateless processes
Each instance must hold no state that another instance needs. If instance 2 keeps your shopping cart in its own memory, the next request landing on instance 3 loses it, and behind a load balancer, requests land anywhere. Push state to a shared backing service (a database, a cache like Redis) and any instance can serve anyone. This is the property that makes horizontal scaling and instant replacement possible.
Factor XI, Logs as event streams
Don't open log files. Don't rotate them. Write events to stdout/stderr as an ordered stream and let the platform capture, route, and store them. When an app writes to a local file, those logs die with the disposable container, exactly the moment you most need them. Streaming to stdout means the platform's log pipeline (Fluent Bit, CloudWatch, Loki) does the hard part.
The local-disk trap
Factors VI and XI are really the same warning twice: the local disk of a cloud process is temporary and unreachable. Anything you write there, a session, an uploaded file, a log, is gone the instant the container is replaced, and the container will be replaced. If it matters, send it somewhere that outlives the process.
What changed since 2011, the 15-factor extension
The original twelve still hold, but the cloud got more demanding. In Beyond the 12-Factor App, Kevin Hoffman added three factors that the modern stack treats as table stakes:
API first, design each service as an API contract before writing the implementation, so other services (and your future self) can build against it.
Telemetry, treat metrics, traces, and structured events as a first-class output of the app, not an afterthought bolted on after an incident.
Authentication & authorization, bake identity and access control into the design from the start; security is a property of the system, not a feature you add later.
None of these contradict the original twelve, they extend the same philosophy (everything-from-outside, observable, contract-driven) into areas that barely existed when Heroku wrote the list.
Applying it: a real Kubernetes deployment
Theory lands when you see it in a manifest. Below is a small but genuinely twelve-factor service: config and secrets injected from the environment (III), a stateless Deployment scaled by replica count (VI, VIII), a port bound and exposed by a Service (VII), graceful shutdown and probes for disposability (IX), and logs left to stream to stdout (XI). Read the comments, each one points back to a factor.
configmap.yaml
yaml
# Factor III: non-secret config lives outside the image.apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
LOG_LEVEL: "info"# plain config, safe in the manifestFEATURE_NEW_CHECKOUT: "true"
---
# Factor III again: secrets are separate and encrypted at rest.apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
stringData:
DATABASE_URL: "postgres://app:CHANGEME@db:5432/app"# Factor IV: a backing service, attached via URLAPI_KEY: "sk_live_xxx"
deployment.yaml
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3# Factor VIII: scale out with more identical copiesselector:
matchLabels: { app: web }
template:
metadata:
labels: { app: web }
spec:
terminationGracePeriodSeconds: 30# Factor IX: time to finish in-flight work on SIGTERMcontainers:
- name: web
image: registry.example.com/web@sha256:abc123 # Factor V: one immutable artifact, pinned by digestports:
- containerPort: 8080# Factor VII: export the service via a portenvFrom: # Factor III: inject config + secrets as env vars
- configMapRef: { name: app-config }
- secretRef: { name: app-secrets }
readinessProbe: # Factor IX: don't send traffic until readyhttpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 3livenessProbe: # Factor IX: restart if it wedgeshttpGet: { path: /healthz, port: 8080 }
periodSeconds: 10# Factor VI: no volumes for state, no local files.# Factor XI: the app just writes to stdout; the platform collects it.
---
apiVersion: v1
kind: Service # Factor VII: route stable traffic to the podsmetadata:
name: web
spec:
selector: { app: web }
ports:
- port: 80targetPort: 8080
Notice what is not here: no environment-specific code, no log-file paths, no baked-in credentials, no local volumes holding state. The image is pinned by digest so the exact thing you tested is the exact thing that runs, Factor X (dev/prod parity) for free. To deploy to another environment you change only the ConfigMap and Secret values; the image is untouched.
verify.sh
bash
# Confirm the deployment honors the factors.
kubectl get deploy web -o jsonpath='{.spec.replicas}'# VIII: scaled to N
kubectl rollout status deploy/web # V: release rolled out cleanly
kubectl logs deploy/web --tail=20# XI: logs come from stdout, not files
kubectl exec deploy/web -- printenv DATABASE_URL # III: config arrived from the environment
kubectl delete pod -l app=web --wait=false # VI/IX: kill a pod; a new one replaces it, no data lost
Common mistakes that cost hours
Hardcoding config in the image. A database URL baked into the code means a rebuild to change environments and secrets leaking into Git. Inject it instead (Factor III).
Storing session or uploads on local disk. The container is disposable; anything on its disk dies on the next restart or rescale. Push state to a backing service (Factor VI).
Writing logs to files. Those files vanish with the container and are unreachable while it lives. Write to stdout and let the platform collect them (Factor XI).
Rebuilding per environment. A separate prod build means you ship something you never tested. Build one image and promote it, changing only config (Factors V & X).
Ignoring SIGTERM. If the app doesn't shut down gracefully, every deploy and rescale drops in-flight requests. Handle the signal and set a grace period (Factor IX).
Scaling up instead of out. Reaching for a bigger machine instead of more copies hits a ceiling fast and gives you no redundancy. Stateless processes scale horizontally (Factor VIII).
Takeaways
The whole article in seven lines
Twelve-factor = self-contained code, everything else injected from outside.
It's why Kubernetes is shaped the way it is, most primitives exist to enforce a factor.
Config (III) lives in the environment, never in code: same image, different values.
Processes (VI) are stateless, push state to a backing service so any copy serves anyone.
Logs (XI) stream to stdout; the platform collects them, the app never manages files.
Build one immutable image and promote it; parity (X) and clean releases (V) come for free.
Hoffman's +3 (API-first, telemetry, auth) extend the same philosophy into the modern stack.
Where to go next
The twelve factors are the philosophy; the labs and tracks below are where you build the muscle memory to apply them on real infrastructure.
Containers & Docker lab, Factors II and X in practice: package dependencies into one image that runs identically everywhere.
This article covers concepts taught hands-on in the Cloud Engineer and DevOps career paths, with real terminal labs, production scenarios, and structured lessons.