Back to Blog
Architecture16 min readApr 2026

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.

ArchitectureCloud Native12-FactorBest Practices
SB

Sri Balaji

Founder · TheSimplifiedTech

On this page

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.
The core idea, in one sentence

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.

git pushbuild oncedeployrun N copiesinject envconnect via URLstdout
Codebase

One Git repo (Factor I)

CI Build

Build stage (Factor V)

Immutable Image

One artifact, all envs

Platform Runtime

Kubernetes / PaaS

Stateless Processes

N identical replicas (VI, VIII)

Config + Secrets

Injected as env vars (Factor III)

Log Stream

stdout collected (Factor XI)

Backing Services

DB, cache, attached (Factor IV)

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. 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. 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. 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. 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. 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.

FactorWhat it meansKubernetes mechanism
I. CodebaseOne repo per app, tracked in version control, deployed many timesOne Git repo → one image per service; GitOps reconciles the cluster to the repo
II. DependenciesDeclare every dependency explicitly; never rely on what's on the hostDependencies baked into the image via the `Dockerfile`; the host stays irrelevant
III. ConfigConfig lives in the environment, never in code`ConfigMap` (non-secret) and `Secret` (encrypted), injected as env vars
IV. Backing servicesTreat databases, caches, queues as attached resources swapped via a URLConnection strings in a `Secret`; a `Service` / external endpoint you point at
V. Build, release, runKeep the three stages strictly separateCI builds the image; a `Deployment` spec is the release; the kubelet runs it
VI. ProcessesRun the app as stateless, share-nothing processesStateless `Pod`s; persistent state pushed out to a database or `PersistentVolume`
VII. Port bindingBe self-contained, export your service by binding to a portContainer `containerPort`; a `Service` routes traffic to it
VIII. ConcurrencyScale out by running more processes, not bigger ones`replicas` count and the `HorizontalPodAutoscaler`
IX. DisposabilityStart fast, shut down gracefully on a signal`SIGTERM` handling, `terminationGracePeriodSeconds`, readiness/liveness probes
X. Dev/prod parityKeep dev, staging, and prod as similar as possibleThe same image in every environment; only injected config differs
XI. LogsTreat logs as event streams to stdout, don't manage filesWrite to stdout/stderr; the node agent (e.g. Fluent Bit) collects and ships them
XII. Admin processesRun 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 manifest
  FEATURE_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 URL
  API_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 copies
  selector:
    matchLabels: { app: web }
  template:
    metadata:
      labels: { app: web }
    spec:
      terminationGracePeriodSeconds: 30   # Factor IX: time to finish in-flight work on SIGTERM
      containers:
        - name: web
          image: registry.example.com/web@sha256:abc123  # Factor V: one immutable artifact, pinned by digest
          ports:
            - containerPort: 8080          # Factor VII: export the service via a port
          envFrom:                         # Factor III: inject config + secrets as env vars
            - configMapRef: { name: app-config }
            - secretRef: { name: app-secrets }
          readinessProbe:                  # Factor IX: don't send traffic until ready
            httpGet: { path: /healthz, port: 8080 }
            initialDelaySeconds: 3
          livenessProbe:                   # Factor IX: restart if it wedges
            httpGet: { 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 pods
metadata:
  name: web
spec:
  selector: { app: web }
  ports:
    - port: 80
      targetPort: 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

  1. 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).
  2. 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).
  3. 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).
  4. 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).
  5. 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).
  6. 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.

Want to go deeper?

This article covers concepts taught hands-on in the Cloud Engineer and DevOps career paths, with real terminal labs, production scenarios, and structured lessons.