05. Build, release, run
1. The rule: build, release, run, in one direction
The factor is one sentence: strictly separate the build and run stages. Turning a codebase into a running deploy happens in three of them, in one direction only: build, then release, then run. Each hands its output forward, with no arrow pointing back.
The build takes the code at a specific commit and turns it into an executable artifact. The release combines that artifact with the deploy’s config and produces something ready to launch. The run takes a release and starts the app’s processes against it. The one-directional flow is the whole constraint, and it has a consequence we’ll return to: you can’t change code in a running deploy, because changed code has to come from the build stage, and there’s no route back to it.
This is the factor where the first three converge. The build starts from a commit of the single codebase Factor 1 insists on; it resolves the dependencies Factor 2 had you declare; and the config Factor 3 kept out of the code joins at the release stage. As the series introduction put it, twelve-factor leans toward operability, and this is operability’s assembly line: the sequence that lets you say precisely what’s running in production and rebuild it on demand.
2. The three stages, and what each one does
The stages divide cleanly by what each one consumes and what it produces.
2.1. Build: a commit becomes an artifact
The build stage transforms the code at a specific commit into an executable build artifact. It does two jobs: it fetches and resolves the declared dependencies, and it compiles the code, gathering binaries and assets into the artifact. A commit goes in, a build comes out.
Both jobs are handoffs from earlier factors. The commit comes from the single tracked history of Codebase, which is what makes a build reproducible: name a commit and you’ve named exactly what goes in. The dependency resolution is Dependencies doing its work, the build reads the dependency manifest and pulls the declared versions, so the artifact carries its libraries with it instead of hoping the run environment has them.
A build can be an involved process, and that’s fine. It’s developer-initiated, started by whoever is shipping the deploy so when a compile fails or a dependency won’t resolve, the error surfaces in the foreground, in front of a person who can read it and fix it. Build complexity always has a human watching it.
2.2. Release: the build plus config
The release stage takes a build, combines it with the deploy’s config, and produces a release that’s ready to run. The build knows nothing about which database or which credentials it will use; the release is where those get attached.
This is exactly where Config‘s “one build, every deploy” cashes out. Factor 3 keeps config in the environment specifically so the build carries none of it, which means one build can feed a release for staging and a release for production, differing only in the config bolted on here. One artifact, many releases, one per deploy. Bake config into the build instead and you get a different artifact per environment, which is a different app per environment wearing one name.
Every release also gets a unique identifier and, once created, is never altered, the two properties the run stage selects against and rollback depends on.
2.3. Run: launch processes against a release
The run stage launches the app’s processes against a selected release, in the execution environment. Pick a release, start its processes, and the app is live. Running an app as one or more processes is its own factor, Processes, but for Factor 5 the point is narrower: the run stage’s only job is to take a release and start it.
The rule for this stage is that it must be kept as boring as possible, and the next section is why.
3. Build can be complex, run must be dead simple
The asymmetry between the two ends of the pipeline is the underappreciated part of this factor: the build can afford to be complex, and the run cannot. The build runs when a developer starts it, with that developer watching. The run stage fires whenever the execution environment decides, and usually nobody is watching at all.
Picture the run stage’s worst trigger. A server reboots at 3am after a kernel update; a process crashes and the process manager restarts it; the orchestrator reschedules a workload onto a fresh machine. No developer initiated any of it, and no developer is awake to see it. If starting the app at that moment means resolving dependencies, compiling an asset or fetching something over the network that might not answer, then a routine 3am restart becomes a 3am outage, failing in front of no one. So the run stage does as little as it can: it takes an already-built, already-configured release and starts processes, nothing more. Everything that can fail in an interesting way has been pushed back into the build, where a human is driving and the error lands in the foreground.
That’s why the separation is worth enforcing even when it feels like ceremony. Front-loading the fallible work into a stage with a person attached keeps the unattended stage boring, and boring is exactly what you want from the thing that runs while you’re asleep.
4. Immutable releases and painless rollback
Every release carries a unique identifier and is immutable: once created, it is never modified. The ID can be a timestamp like 2011-04-06-20:32:17 or an incrementing counter like v100; what matters is that each release has one and no two share it. Change anything, a new config value or a rebuilt artifact, and you don’t edit an existing release. You append a new one with a new ID. Releases only ever get added, never rewritten.
That append-only ledger is the point, because it makes rollback trivial. Every release the app has ever run is still there, whole and unchanged, addressable by its ID. Rolling back isn’t a rebuild; it’s repointing the run stage at an earlier release and restarting. The release you want is already built, already configured, and already known to work, because it ran before. You don’t recover a good state, you reselect one.
This is the immutability half of the one-directional flow. Because a release can’t be mutated in place, and because run has no path back to build, the only way to change what’s running is to append a new release and point at it. You move forward by appending and back by reselecting, but you never reach in and alter a release that already exists.
5. Every modern CI/CD pipeline has this shape
The three stages, written down in 2011, are the shape of the CI/CD and container pipeline the industry later converged on. The mapping is close enough to be startling:
| Build, release, run (2011) | Modern container pipeline |
|---|---|
| Build: a commit becomes an artifact | docker build produces an immutable container image |
| Release: build plus config, with a unique ID | The image bound to its config and a deployment manifest, which Kubernetes stamps with a revision (the pod template, not the ConfigMap contents) |
| Run: launch processes against a release | The scheduler starts a pod from that release |
| Rollback: reselect a prior release | kubectl rollout undo restores the previous pod template |
Read down the right column and it’s all Factor 5: an immutable image as the build artifact, config and a manifest joined at release (Kubernetes even stamps a revision, its version of the unique release ID), a pod as the run. That revision captures the pod template, the image plus references to ConfigMaps and Secrets, not the contents those references point at, which is why editing a live ConfigMap (the next section) can change what runs without cutting a new release. Rollback survived most literally of all, kubectl rollout undo reselects the prior pod template instead of rebuilding, restoring its references rather than the ConfigMap contents as they were then, exactly the append-only idea from the last section.
None of this was designed from the essay. Docker and Kubernetes arrived years later and converged on its shape because that shape is what falls out of taking build, release, run seriously: an immutable artifact, config joined late, releases you can reselect. The 2011 model reads like documentation for tools that didn’t exist yet.
6. The discipline most teams break: editing a live release
The immutable release is also the part teams break most often, and breaking it produces exactly the failure the factor exists to prevent. The separation looks like ceremony right up until an incident, and under pressure the ceremony is the first thing to go.
Here’s the concrete version. Something’s wrong in production, so someone runs kubectl edit configmap and changes a value in place, or SSHes into the box and hand-edits an env var like API_KEY=<redacted>, and the incident clears. Neither touches a revision, so the running deploy is now a release that no build ever produced. Nothing in git describes it, no release ID names it, and the artifact plus config actually serving traffic exist only in the memory of whoever typed the fix. That’s a release mutated in place, the exact thing the append-only rule forbids, and the ledger that let you reselect a known-good state now has an unrecorded gap in it.
Do that a few times across a few machines and you get config drift: instances that were meant to be identical have quietly diverged, each carrying a different hand-fix. That’s the snowflake server, a production box in a state no pipeline can reproduce, because the state was never a release. When it finally breaks, you can neither roll back to it nor rebuild it, because it never came from a build. This is the “it worked until someone changed it live and now we can’t reproduce it” outage, and it’s the direct cost of editing a running deploy instead of cutting a new release. The fix is never clever: change the config or the code, run it back through build and release, and deploy a new release with a new ID. Slower in the moment, reproducible forever after.
That’s what the discipline buys: a deploy you can name, reproduce, and undo. You can say exactly what’s running and rebuild it, and you can return to any release you’ve ever run.