There is a short document from 2011 called the Twelve-Factor App, and almost every “why does it work on my machine but not in production” bug you will ever hit is a factor you broke. It is not a framework or a tool. It is twelve habits for building an app that runs the same way on your laptop, on a teammate’s laptop, and on a server you have never logged into. Fifteen years on it is still the backbone of how cloud apps are built, which is rare enough to be worth an afternoon.
Why “it works on my machine” is a methodology problem
The sentence every developer has said, and every developer has heard with dread, is “but it works on my machine”. It almost always means the same thing: the app quietly depends on something that only exists on your machine. A password typed into a file. A database running in the background you forgot you started. A library you installed by hand a year ago. The Twelve-Factor App is, more than anything, a list of those hidden dependencies and a rule for each one.
Picture a recipe written so it produces the same dish in any kitchen. It cannot say “add a pinch of that spice on my counter”, because your counter is not everyone’s counter. It has to name every ingredient and every amount, so the kitchen does not matter. Twelve-Factor is that discipline applied to software: nothing about how the app runs is allowed to hide on one particular machine.
One codebase, many running copies
The first habits are about the relationship between your code and the copies of it that run.
- Codebase. One app, one repository, tracked in Git. From that single codebase you get many deploys: a deploy is just one running copy, whether that is production, a staging site, or your laptop. Same code, different places.
- Dependencies. Every library the app needs is declared in a file (
package.json,requirements.txt), never assumed to be already on the machine. A new laptop runs one install command and has exactly what the app needs, nothing more. - Build, release, run. Turning code into a running app happens in three separate stages, and this separation is the one people underrate. Build compiles the code into a bundle. Release combines that bundle with the config for one environment. Run starts it. Keeping them separate is what lets you roll back instantly: a release is a numbered, frozen thing, so going back to yesterday is picking release 41 again, not rebuilding and hoping.
flowchart LR C[One codebase in Git] --> B[Build: code becomes a frozen bundle] B --> R[Release: bundle + this environment's config] R --> N[Run: the release starts as processes] CFG[Config from the environment] --> R R -.numbered, so you can roll back.-> R
The three things that must never live on your laptop
This is the heart of it, and the cure for “works on my machine”. Three factors, one idea: anything that changes between environments lives outside the code.
- Config in the environment. Anything that differs between your laptop and production (database addresses, API keys, which payment mode to use) is read from environment variables, not written into the code. The same build then runs anywhere, because the environment tells it who it is. A password in the code is both a bug and a security hole, since the code is shared and the password should not be.
- Backing services are attached resources. A backing service is anything the app talks to over the network: the database, the cache, the email sender, file storage. Treat each as a resource you attach by a URL in the config, not something baked in. Swapping a local database for a hosted one, or a broken cache for a fresh one, should be a config change and nothing else.
- Dev/prod parity. Keep development, staging, and production as alike as you can. The bugs that cost the most are the ones that only appear in production because production is running a different database version, or a different operating system, from the one you tested on. This is the factor Docker made easy, and the reason so many teams reach for it.
CAUTION
A secret in your code is the most common way this rule gets broken, and the most expensive. The
moment a key is committed to Git it is in the history forever, even if you delete it in the next
commit. Keep secrets in the environment from the very first commit, and put your .env file in
.gitignore before you write anything into it.
Treat every process as throwaway
The next group is about how the running app behaves, so a platform can start, stop, and multiply it freely.
- Processes are stateless. The app keeps nothing important in its own memory between requests. Anything that must survive goes into a backing service, usually the database. Why it matters: if a request only works because an earlier request left something in memory, that app cannot be safely restarted or copied, and both of those happen constantly in the cloud.
- Port binding. The app is self-contained and offers its service on a port itself, rather than needing to be slotted inside a separate web server. It is a complete thing you can run, not a plugin that only works in one particular host.
- Concurrency. To handle more load, run more copies of the process rather than making one copy bigger. Because the processes are stateless, ten copies behave like one, and a platform can add and remove copies as traffic rises and falls. This only works because of the stateless rule above.
- Disposability. Processes should start fast and shut down gracefully. When a shutdown signal arrives, the app should stop taking new work, finish what it is holding, and exit. Fast, clean starts and stops are what let a platform move your app around without users noticing.
IMPORTANT
Stateless is the factor that ties the group together. Get it right and concurrency, disposability, and painless restarts all come almost for free. Get it wrong, by storing a user’s session in the process memory, and none of the others can save you.
Watch it and manage it from outside
The last two are about running the app once it is live.
- Logs as event streams. The app does not manage its own log files. It just writes events to the screen (standard output) as a stream, and the platform around it decides where that stream goes: a search tool, a dashboard, an archive. The app’s job is to emit; routing is someone else’s job.
- Admin processes. One-off jobs like a database migration or a data fix run as separate processes against the same release and the same config as the app, not as a script you paste into a console with different settings. The one-off task and the running app share an environment, so they cannot disagree about the world.
The twelve, in one table
| # | Factor | The habit | The bug it prevents |
|---|---|---|---|
| 1 | Codebase | One repo, many deploys | ”Which copy is the real one?“ |
| 2 | Dependencies | Declare them all in a file | ”You have to install X by hand first” |
| 3 | Config | Read it from the environment | A secret committed to Git |
| 4 | Backing services | Attach by URL in config | Cannot swap the database without a code change |
| 5 | Build, release, run | Three separate stages | Cannot roll back cleanly |
| 6 | Processes | Stateless, share nothing | Breaks when restarted or copied |
| 7 | Port binding | Self-contained, offers a port | Only runs inside one special host |
| 8 | Concurrency | Scale by adding copies | One giant process you cannot grow past |
| 9 | Disposability | Fast start, graceful stop | Lost work every deploy |
| 10 | Dev/prod parity | Keep environments alike | ”It only breaks in production” |
| 11 | Logs | Write a stream, let the platform route it | Logs trapped on a server nobody checks |
| 12 | Admin processes | One-off jobs share the release | A migration run with the wrong settings |
The four factors that show their age in 2026
The methodology has aged unusually well, and most of it is now simply default practice. But it was written in 2011, before some things we now take for granted, and honesty about the gaps is part of using it well.
- Logs are no longer enough. Modern observability stands on three pillars: logs, metrics, and traces. Twelve-Factor mentions only logs. A 2026 app also emits metrics (numbers over time) and traces (the path of one request across services), usually through a standard called OpenTelemetry. Treat metrics and traces as streams the same way, and you are extending factor eleven, not breaking it.
- Environment variables are not a secrets manager. Reading config from the environment was radical in 2011 and is right for most settings. But real apps now use a dedicated secrets manager for the sensitive parts, plus feature flags for values that change while the app runs. Env vars are the floor, not the whole building.
- It says nothing about the API. Authentication, rate limiting, and API versioning are non-negotiable for anything exposed to the internet today, and the twelve factors do not mention them. That is not a flaw so much as a scope: they describe how an app is run, not how its front door is guarded.
- It assumes backing services are always up. Modern systems plan for the database or the payment provider being briefly unavailable, with retries, timeouts, and graceful degradation. The original twelve quietly assume the network works.
NOTE
None of these gaps make the twelve wrong. They make them a foundation you build on. A 2026 app that follows all twelve and adds observability, a secrets manager, and sensible retries is in very good shape, and it got most of the way there on a checklist older than some of the frameworks you use.
The tools you actually reach for
| Job | Popular tools | Note |
|---|---|---|
| Config from the environment | .env files with dotenv, platform env settings | Free; the .env file stays out of Git |
| Secrets, done properly | Doppler, HashiCorp Vault, AWS/GCP secret managers | Beyond plain env vars for sensitive values |
| Dev/prod parity | Docker, Docker Compose | The standard way to make environments match |
| Build, release, run | GitHub Actions, any CI/CD | Free tier; keeps the three stages separate |
| Observability (the modern add-on) | OpenTelemetry, Grafana, Sentry | OpenTelemetry is the open standard for metrics and traces |
| The source itself | 12factor.net | The original, still short and worth reading in full |
An old checklist that still finds your bugs
The lasting lesson is that most production pain is not exotic. It is a hidden dependency on one machine, a secret in the code, a process that cannot be restarted, an environment that does not match the one you tested on. The Twelve-Factor App named all of those in 2011 and the names still hold. It pairs naturally with the shipping stop and the monitoring stop of the full-stack roadmap: those are how you put an app online and keep it alive, and this is the shape the app has to be in first. Read the original once, keep the table nearby, and treat the four gaps as your homework for the modern half.
Build it so the machine never matters.
Sources
- 12factor.net for the original methodology and the definition of each factor
- ITNEXT: the 12-factor app 15 years later, does it still hold up in 2026 for which factors have become default and which show their age
- Honeycomb: twelve-factor apps and modern observability for extending logs to metrics and traces
- OneUptime: understanding the twelve-factor app methodology (2026) for a factor-by-factor modern walkthrough
- 12factor.net/blog: evolving twelve-factor to modern cloud-native platforms for the modernization effort and its gaps