DEV Community

Cover image for The 12-Factor App, The Rulebook Behind Every Cloud-Native Deploy
Sri Balaji
Sri Balaji

Posted on Originally published at thesimplifiedtech.com

The 12-Factor App, The Rulebook Behind Every Cloud-Native Deploy

TL;DR: Understand the twelve factors behind every cloud-native deploy, why Kubernetes was built around them, and the three that bite people most. Walk away able to apply them to a real Deployment manifest, plus the 15-factor update.

Contents

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.

In the real world In tech
A rental car: any one in the lot will do, you bring nothing into it, and if it breaks you swap it for another A 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 swap Never 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 pickup The platform injects config and credentials as environment variables at startup
Every car in the fleet is the identical model, so the experience is the same One 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 pl

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 Pods; 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
IX. Disposability Start fast, shut down gracefully on a signal SIGTERM handling, terminationGracePeriodSeconds, readiness/liveness probes
X. Dev/prod parity 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.

💡 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

# 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"
Enter fullscreen mode Exit fullscreen mode

deployment.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
Enter fullscreen mode Exit fullscreen mode

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

# 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
Enter fullscreen mode Exit fullscreen mode

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.


Originally published on TheSimplifiedTech, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.

Top comments (0)