cd ../blog

23 Oct 2026 · 3 min read · Ravi Sanchala

Build once, deploy everywhere

If staging and production each get their own build, you never shipped what you tested. Immutable, versioned releases fix that.

releasesci-cdgo

Here’s a question worth asking about your deploy process: is the thing running in production the exact same file you tested in staging?

On a lot of teams the honest answer is “probably”. Each environment gets its own build from the same commit, and everyone assumes the results are identical. Usually they are. When they aren’t, you get the worst kind of bug: one that only exists in production.

The problem with rebuilding per environment

When every deploy rebuilds from the working tree:

  • Staging and production run different binaries. Same commit, different build, different moment. A dependency update or a changed build flag can sneak in between them.
  • Nothing is version-addressable. “Reinstall last week’s version” means checking out an old commit, rebuilding and hoping the result matches.
  • Builds can include things that aren’t in Git. A local uncommitted change, an untracked file, a different tool version on someone’s laptop.

Build once, publish immutably

The fix is to separate making a release from installing a release.

  1. Release: build the artifact once, give it a version number, and publish it to storage (a bucket or registry) where it can never be overwritten.
  2. Install: put a specific, already-published version onto an environment.

Staging installs version 1.4.2. You test it. Production installs version 1.4.2: the same bytes, not a rebuild.

Keep config out of the artifact

This only works if the build is environment-neutral. The binary shouldn’t know whether it’s in staging or production.

Environment differences (database host, feature flags, log level) live in configuration generated per environment from a committed template. The release carries the binary once, plus each environment’s config. Secrets stay in a secret manager and are read at runtime.

Guardrails that make versions trustworthy

A version number is only useful if it really points to one exact thing. A few rules enforce that:

  • Refuse to release from uncommitted changes. If the working tree is dirty, the release can’t be traced back to a commit.
  • Refuse to release a commit that only exists on one laptop. Push first, so anyone can check out what shipped.
  • Get the next version from the release store, not a file in the repo. A version file that every release edits becomes a merge conflict on every branch.
  • Never overwrite a published version. If two people release at once, the second one fails loudly instead of silently replacing the first.
  • Tag the commit. An annotated Git tag records which commit a version came from, so the repository alone can answer “what was in 1.4.2?”

Make the install step boring too

  • The version is always explicit. No “install latest”. What goes to production is a decision someone made, visible in the command history.
  • Database migrations run before the new version goes live, and they’re tested against a throwaway database before the release is published.
  • A health check runs after install. If the new version doesn’t answer, the previous one stays in place.

What you get

  • What you tested is exactly what you shipped
  • Rolling back is installing an older version number, not reconstructing one
  • Anyone on the team can answer “what’s running in production?” in one command

It takes a little discipline to set up. After that, releases stop being events and start being routine. That’s what you want.

related case study From hand-built servers to multi-country infrastructure, entirely as code read case study →

$ ./book-call.sh

Dealing with something like this?

Let's talk it through. 30 minutes, free, and you'll leave with a clear picture of what to fix first.

Book a free 30-min callOr take the free 3-minute scorecard

next post

Build, then configure: why OpenTofu and Ansible stay separate Provisioning servers and configuring them are different jobs. Keeping them in two phases makes both safer to change and easier to fix. Read post →