The twelve-factor methodology was written by Heroku engineers around 2011, back when "the cloud" mostly meant renting a VM and hoping. Fifteen years later, most of it reads like it was written yesterday — because the failure modes it guards against haven't changed. Apps still break in production because config was baked into a build. Deploys still go sideways because someone SSH'd into a box and edited a file.
The original text lives at [12factor.net](https://12factor.net) and it's short — read it once. This post is the practical companion: what each factor actually means when you're deploying to a managed platform in 2026, which factors matter more now, and which ones the platform quietly handles for you.
The factors that still do the heavy lifting
I. Codebase — one repo, many deploys
One codebase tracked in version control, deployed many times (staging, production, preview). The modern violation isn't multiple repos for one app — it's the opposite: config repos, "deploy repos," and infrastructure snowflakes that drift from what's in Git. If your production state can't be reconstructed from the repo plus environment config, you've broken factor I even if you technically have one repo.
On a git-push platform this factor is almost enforced for you: the deploy *is* the repo at a commit. Rollback means redeploying an earlier commit, not un-editing a server.
II. Dependencies — declare everything, vendor nothing implicitly
Explicitly declare and isolate dependencies. In 2026 the lockfile is the contract:
npm ci # not npm install — installs exactly the lockfile
pip install -r requirements.txt
go mod downloadThe modern extension is the *build environment itself*. A Dockerfile pins your OS packages and runtime version the same way a lockfile pins libraries. If you rely on buildpacks instead, make sure your runtime version is pinned in package.json (engines), .python-version, or go.mod — "whatever Node the builder had that day" is a dependency you didn't declare.
III. Config — environment variables, still
Config is everything that varies between deploys: database URLs, API keys, hostnames. It goes in environment variables, never in code, never in a committed file.
The test from the original essay is still the best one: could you open-source your repo right now without leaking a credential? If not, config is in the wrong place.
What's changed since 2011 is tooling, not principle. Secrets managers, sealed secrets, and platform env-var UIs all exist so that the *app* still just reads process.env. Keep the app dumb. On PandaStack, for example, environment variables are set per-app in the dashboard, and when you attach a managed database the platform injects DATABASE_URL itself — the app never knows or cares where the value came from.
IV. Backing services — everything is an attached resource
Your database, cache, queue, and email provider are all just URLs in config. Swapping a local Postgres for a managed one should be a config change, zero code changes.
The practical rule: connect via one URL, not five discrete variables. A single DATABASE_URL is portable across every platform and every ORM. If your framework wants DB_HOST/DB_PORT/DB_USER separately, parse the URL at boot rather than maintaining two config formats.
V. Build, release, run — strict separation
Build compiles the code. Release combines the build with config. Run executes it. The rule that matters day-to-day: you cannot change code at runtime. No hot-patching a container, no editing files on a server.
The corollary people miss: releases are append-only. Every deploy gets an ID, and rollback means re-running an old release, not reverse-engineering what changed. Platforms give you this as deployment history — use it as your incident escape hatch instead of "fix forward under pressure."
VI. Processes — stateless, share-nothing
The process can crash, be rescheduled, or run as ten copies, and nothing is lost — because session state lives in Redis or the database, and uploaded files live in object storage, not on local disk.
This factor got *more* important, not less. Scale-to-zero platforms will literally stop your process when idle and start a fresh one on the next request. Sticky sessions and in-memory caches that were "mostly fine" on a long-lived VM are correctness bugs now. If your app can't survive its process being replaced mid-afternoon, fix that before anything else on this list.
IX. Disposability — fast startup, graceful shutdown
Processes start fast and shut down cleanly on SIGTERM:
process.on('SIGTERM', async () => {
server.close(); // stop accepting new requests
await inflight.drain(); // finish what's running
await db.end(); // release connections
process.exit(0);
});Kubernetes sends SIGTERM, waits (30s by default), then SIGKILLs. An app that ignores SIGTERM drops in-flight requests on every single deploy — a downtime bug that looks like random client errors. Fast startup matters for the same reason in reverse: on scale-to-zero infrastructure, your boot time *is* your cold-start latency.
XI. Logs — write to stdout, let the platform aggregate
The app never manages log files, rotation, or shipping. It writes an unbuffered event stream to stdout and the execution environment handles routing. Every log agent, every managed platform, every kubectl logs invocation assumes this. The moment you write to ./logs/app.log inside a container, those lines die with the pod.
Structured (JSON) logging is the 2026 upgrade to this factor — same principle, but each line is queryable. On PandaStack the stdout stream is captured and viewable live in the dashboard, both during builds and at runtime, which is exactly the division of labor factor XI prescribes: app emits, platform aggregates.
The factors the platform mostly absorbs
VII. Port binding & VIII. Concurrency
Bind to the port in the PORT env var, listen on 0.0.0.0, and scale by running more processes rather than making one process bigger. On container platforms this is table stakes — the router expects a port, the autoscaler adds replicas. Your only jobs: read PORT instead of hardcoding, and don't assume a single instance (see factor VI).
X. Dev/prod parity
Keep development and production close: same database engine, same version, minimal time between writing code and deploying it. The cheap 2026 version is Docker for backing services locally — run Postgres 16 in a container because production runs Postgres 16, not SQLite "because it's easier." Most ORM bugs I've debugged in the last few years were parity violations: a query that worked on SQLite and failed on Postgres.
XII. Admin processes
One-off tasks (migrations, console sessions, data fixes) run in the same environment, same code, same config as the app — as a release step or a one-off command, never as a script someone runs from their laptop against production with hand-copied credentials.
The honest scorecard
Two factors have aged into footnotes. Factor I's "one codebase" needs asterisks in a monorepo world, and factor VIII's process-type model (web, worker) has been generalized by containers into "whatever you can run." Everything else stands, and three of them — config in the environment (III), stateless processes (VI), and disposability (IX) — have become *more* load-bearing as platforms got more dynamic. Autoscaling, spot instances, and scale-to-zero all assume your app treats its own process as expendable.
A useful exercise: score your current app against the twelve factors before your next platform migration. Apps that score well move between platforms in an afternoon. Apps that don't reveal exactly where the work is — and it's almost always factors III, VI, and IX.
If you want to see how a twelve-factor app behaves on a platform built around these assumptions — env-var config, injected DATABASE_URL, stdout logs, git-push deploys — you can try one on [pandastack.io](https://pandastack.io).