# Build & Release Basics

> How source code becomes a thing you can actually ship: what a build produces, why you version and freeze artifacts, and how the same artifact moves from dev to staging to prod.


---

# Build & Release Basics

You wrote some code. It runs on your laptop. And then someone asks you to "ship it" or "push it to prod" - and suddenly there's talk of *builds* and *artifacts* and *staging* and *promoting a release*, and none of it was in the tutorial that taught you the language. That gap is normal, and it's not your fault: nobody hands you the manual for the journey from "code on my machine" to "a running thing other people use."

This guide closes that gap. By the end you'll understand what a build actually produces, why you give releases version numbers and then freeze them, and how the very same built thing travels from your machine to staging to production without being rebuilt at each stop. None of it is magic - it's a small set of ideas that, once you see them, make the whole pipeline obvious.

## How to read this
- **Want it to finally make sense?** Read in order - each phase builds on the last, starting from what a build even *is*.
- **Just need the one idea that explains the most pain?** Phase 3 covers why "works in staging, breaks in prod" happens, and the rule that prevents it.

## The phases
1. **[What "Building" Actually Produces](01-what-building-produces.md)** - turning source code into a runnable, shippable *artifact*, and why a clean, repeatable build matters.
2. **[Versions & Artifacts](02-versions-and-artifacts.md)** - giving a release a version number, freezing the artifact so it never changes, and where built artifacts live.
3. **[Environments & Promotion](03-environments-and-promotion.md)** - dev, staging, and prod; promoting the *same* artifact through them instead of rebuilding; and keeping config separate from code.

> This guide stops at the moment you have an artifact ready to move through environments. *Who* pushes the buttons, and how to make all of this automatic, is the next guide: [What CI/CD Does](/guides/what-cicd-does).


---

# What "Building" Actually Produces

Here's the thing nobody says out loud: the code you write is *not* the thing that runs. The files in your editor are written for humans - for you, your teammates, the person who reads them next year. The computer that actually serves your app at 3am wants something different: a packaged, ready-to-run thing. **Building is the step that converts one into the other.**

Once you hold that idea - *source goes in, a runnable thing comes out* - the word "build" stops being intimidating. Let's look at what comes out, and why the *way* it comes out matters as much as the result.

## A build is a recipe, and the artifact is the meal

**What it actually is.** A build is a recipe: a fixed series of steps that takes your source code (the ingredients) and produces one packaged result (the meal). That result has a name - an **artifact**.

📝 **Terminology.** An *artifact* is the output of a build: a single, concrete file or package that you can run or ship. "Source code" is what you write; the "artifact" is what you deploy. They are not the same thing, and keeping them straight is most of this guide.

**What it does in real life.** The exact recipe depends on your language and platform, but it almost always does some mix of these jobs:

- **Compiling** - translating source code into the lower-level instructions a machine actually executes. (Languages like Go, Rust, Java, and C# do this.)
- **Bundling** - gathering many source files plus their dependencies into one (or a few) optimized files. (This is what a JavaScript/TypeScript front-end build does.)
- **Packaging** - wrapping the result, and sometimes the environment it needs to run in, into one shippable unit (like a container image).

The artifact you get depends on the job:

```mermaid
flowchart LR
  Go[main.go] -->|go build| GoBin[a compiled binary]
  TS[src/*.ts, *.css] -->|npm run build| Bundle[a bundle: dist/ folder]
  Docker[Dockerfile + app] -->|docker build| Image[a container image]
```

The point of the diagram: different stacks produce different *kinds* of artifact, but it's always the same move - source in, one runnable thing out.

## A real example: building a small program

Let's watch a build happen. Here's a compiled language (Go), because the before-and-after is so clear:

```console
$ ls
go.mod  main.go

$ go build -o hello

$ ls
go.mod  hello  main.go

$ ./hello
Hello, world!
```

*What just happened:* The first `ls` showed only source files - `main.go` is human-readable text you could open in any editor. `go build` ran the recipe: it read your source, compiled it, and wrote out a brand-new file called `hello`. That file is the artifact. The final `./hello` *ran the artifact directly* - notice you didn't run `main.go`, you ran the built thing. The source was the recipe; `hello` is the meal.

A front-end build looks different on the surface but is the same idea:

```console
$ npm run build

> build
> vite build

vite v5.0.0 building for production...
✓ 34 modules transformed.
dist/index.html                  0.46 kB
dist/assets/index-a1b2c3d4.js   143.21 kB
✓ built in 1.84s
```

*What just happened:* The build read your TypeScript, CSS, and component files, transformed and combined them, and wrote the result into a `dist/` folder. That `dist/` folder is the artifact - a set of plain files a web server can hand to browsers. Your original source files are still there, untouched; the build *produced* something new alongside them. (The exact file names and sizes you see will be your own - never trust a number you didn't run.)

## Why a clean, repeatable build matters

**What it actually is.** A build is *repeatable* (people also say **reproducible**) when running the same recipe on the same source gives you the same artifact every time - on your machine, on a teammate's, on a server in the cloud. The recipe doesn't secretly depend on something that only exists on your laptop.

**Why people get this wrong.** The classic trap is the build that works because of something *unwritten* - a tool you installed by hand months ago, an environment variable only you have set, a file that lives on your Desktop. The build passes for you and fails for everyone else. This is the original "works on my machine," and it's a build problem: the recipe was incomplete.

⚠️ **The gotcha: hidden ingredients.** If your build needs something, that something must be declared *in the project* - in a dependency file, a lockfile, a `Dockerfile` - not assumed to already be present. A reproducible build is one a stranger could run on a fresh machine and get the identical result. The moment a build depends on a hidden ingredient, it stops being a recipe and becomes a magic trick that only works in your kitchen.

📝 **Terminology.** A *lockfile* (like `package-lock.json` or `Cargo.lock`) records the *exact* version of every dependency your build used. It's how you make "install the dependencies" mean the same thing today and six months from now, instead of silently pulling newer versions that behave differently.

**Why this saves you later.** Almost every "but it built fine yesterday" or "it builds for me, not in CI" panic traces back to a build that wasn't truly self-contained. When your build is a clean, declared recipe, the artifact becomes *trustworthy* - and that trust is exactly what the next phase relies on when we freeze an artifact and ship it everywhere.

## Recap

1. **The code you write isn't the thing that runs.** A *build* converts source into a runnable **artifact**.
2. **A build is a recipe; the artifact is the meal.** Depending on your stack, the artifact is a compiled binary, a bundle of files, or a container image.
3. **Different stacks, same move:** source in, one shippable thing out (`go build`, `npm run build`, `docker build`).
4. **A clean build is reproducible:** same recipe + same source = same artifact, anywhere - because every ingredient is declared, not assumed.

Now that you can produce a trustworthy artifact, the next question is: how do you *name* it, and how do you make sure the thing you tested is the exact thing you ship?


---

# Versions & Artifacts

You've got an artifact - a trustworthy, runnable thing your build produced. Now real life shows up. Someone reports "the bug in the version from last Tuesday." Someone asks "are we running the same build on the two servers?" Someone needs to roll back to "the one before the broken one." Every one of those sentences needs the artifact to have a *name* you can point at, and a guarantee that the named thing never quietly changes underneath you.

That's what this phase is about: numbering releases so humans can talk about them, and freezing artifacts so the words still mean something tomorrow.

## Versioning: a name everyone can agree on

**What it actually is.** A version is a label you stamp on a release so that a specific build has a name. Instead of "the build from the deploy that Priya did, you know, the afternoon one," everyone can say "**1.4.0**" and mean the exact same thing.

**The common convention: semantic versioning.** The most widespread scheme is **semantic versioning** (often written *semver*): three numbers separated by dots, like `2.5.1`. Each number carries a meaning:

```text
        2  .  5  .  1
        │     │     │
        │     │     └── PATCH  → backward-compatible bug fixes only
        │     └──────── MINOR  → new features, nothing existing breaks
        └────────────── MAJOR  → a breaking change; users may need to adapt
```

The promise is simple and humane: the version number tells you *how scared to be* about upgrading. A jump from `2.5.1` to `2.5.2` is a safe bug fix. `2.5.1` to `2.6.0` adds something new but won't break what you had. `2.5.1` to `3.0.0` is a warning: something changed in a way that might break you, so read the notes before you upgrade. (The full rules live at [semver.org](https://semver.org).)

📝 **Terminology.** *Backward-compatible* means existing users keep working without changing anything. A *breaking change* is the opposite: the new version drops or changes something people relied on, so they have to adjust. The whole point of the MAJOR number is to make breaking changes loud instead of surprising.

**Why this saves you later.** When you can say "we're on `1.4.0`, the bug appeared in `1.5.0`, roll us back to `1.4.0`," an outage becomes a calm, precise conversation instead of a scramble. Version numbers turn "some build, somewhere" into something you can reason about under pressure.

## Immutable artifacts: build once, never change it

**What it actually is.** An artifact is **immutable** when, once it's built and labeled, it never changes - not one byte. Version `1.4.0` is `1.4.0` forever. If you need a fix, you build a *new* artifact with a *new* version (`1.4.1`); you never reach back and edit `1.4.0` in place.

📝 **Terminology.** *Immutable* just means "cannot be changed after it's created." An immutable artifact is a frozen photograph of one build, not a document people keep editing.

**Why people get this wrong.** It's tempting to treat the deployed thing as something you tweak: SSH into the server, edit a file, "just patch it live." The moment you do, the running thing no longer matches *any* version you have a name for. Now "we're running `1.4.0`" is a lie, and nobody can reproduce, test, or roll back what's actually live. The version label has come unstuck from reality.

**The rule that fixes it: build once, deploy the same thing everywhere.** You build the artifact a *single* time, freeze it, and then move that *identical* frozen artifact wherever it needs to go. You do not rebuild it for testing and then rebuild it again for production - that would produce two different artifacts that you only *hope* are the same.

```mermaid
flowchart LR
  Source[source] -->|build once| Frozen[1.4.0 frozen]
  Frozen -->|deploy the same artifact| Test[test server]
  Frozen -->|deploy the same artifact| Prod[prod server]
```

⚠️ **The gotcha.** "We rebuild for production" sounds responsible, but it quietly breaks the chain of trust. The artifact you *tested* and the artifact you *shipped* came out of two separate build runs - a dependency could have updated, a tool could differ, the clock could have ticked past something. You tested one thing and shipped another. The whole reason Phase 1 insisted on reproducible builds is so you *don't have to* rebuild: you build the trustworthy artifact once and carry that exact one forward.

**Why this saves you later.** Immutability is what makes a rollback real. If `1.5.0` is broken, you can redeploy the untouched `1.4.0` artifact and *know* it's exactly what worked before, because nothing was allowed to change it. "Build once, deploy everywhere" is the single rule that makes the rest of releasing trustworthy - and it's the setup for Phase 3.

## Artifact registries: where built things live

**What it actually is.** Once you've built and frozen an artifact, it needs a home - somewhere central that every machine can fetch the exact same copy from. That home is an **artifact registry**: a storage system for built artifacts, organized by name and version.

**What it does in real life.** You build an artifact, push it to the registry under a name and version, and from then on any server, teammate, or automated system can pull *that specific version* back down. The registry is the source of truth for "what does `1.4.0` actually consist of."

You've almost certainly used one already. A **container registry** like Docker Hub stores container images:

```console
$ docker build -t myapp:1.4.0 .

$ docker push myapp:1.4.0
The push refers to repository [docker.io/yourname/myapp]
9f2a1c7e: Pushed
1.4.0: digest: sha256:3b1e... size: 1574
```

*What just happened:* `docker build` produced an image artifact and labeled it `myapp:1.4.0`. `docker push` uploaded that exact image to the registry. From now on, anyone (or any server) can run `docker pull myapp:1.4.0` and receive the *identical* image - note the `digest: sha256:...`, a fingerprint of the exact bytes, so there's no doubt two machines got the same thing. The registry is how "the same artifact everywhere" stops being a wish and becomes a fact.

📝 **Terminology.** A *registry* stores built artifacts (Docker Hub for container images, npm for JS packages, a Maven repository for Java, and so on). It is *not* the same as a *Git repository*, which stores your *source code*. Source lives in Git; the built artifacts live in a registry. Keeping those two straight clears up a lot of early confusion.

**Why this saves you later.** When the artifact lives in a registry under a fixed version, "deploy `1.4.0` to that new server" is a download, not a rebuild. The registry is the bridge between "we built it once" and "it now runs identically in five places" - which is exactly the trip we take in the next phase.

## Recap

1. **Version your releases** so a specific build has a name everyone shares. **Semantic versioning** (`MAJOR.MINOR.PATCH`) encodes how risky an upgrade is.
2. **Artifacts are immutable:** once built and labeled, they never change. A fix means a new version, never an edit to the old one.
3. **Build once, deploy the same artifact everywhere** - rebuilding per destination silently breaks the link between what you tested and what you shipped.
4. **An artifact registry** is where frozen artifacts live, so any machine can pull the exact same version on demand.

You now have a named, frozen artifact sitting in a registry. The last question is the one that bites everyone: how do you move that one artifact through dev, staging, and production - and why does it sometimes work in one and break in the next?


---

# Environments & Promotion

Your artifact is built, frozen, and sitting in a registry. So where does it go? Not straight to your users - that would be terrifying. Instead it makes a journey through a few separate copies of your running system, each one a slightly more serious dress rehearsal than the last. Understanding those copies, and the one rule for moving an artifact between them, is what turns "deploying" from a leap of faith into a routine.

This phase also answers the single most common confusion in all of releasing - the dreaded "but it worked in staging!" - and shows you why it happens and how to stop dreading it.

## Environments: rehearsal stages for your software

**What it actually is.** An **environment** is a complete, separate place where your software runs. The same artifact can run in several environments at once, each isolated from the others, so what you do in one can't touch the rest. The usual three:

```mermaid
flowchart LR
  Dev[Dev<br/>your sandbox - break freely] -->|promote| Staging[Staging<br/>a mirror of prod - final rehearsal]
  Staging -->|promote| Prod[Production<br/>the real thing - real users & data]
```

- **Dev (development)** - your sandbox. Fake or sample data, fast feedback, safe to break. This is where you confirm the thing runs at all.
- **Staging** - a deliberate mirror of production, built to be as close to the real thing as possible. The final dress rehearsal: if it's wrong here, you want to find out *now*, not in front of users.
- **Production (prod)** - the real system. Real users, real data, real consequences. This is the performance the rehearsals were for.

📝 **Terminology.** People shorten these constantly: *dev*, *staging* (sometimes *stage*), *prod*. "It's in prod" means it's live for real users. "Push to staging" means deploy to the rehearsal environment first.

**Why this saves you later.** Environments exist so mistakes have somewhere cheap to happen. A bug caught in dev costs you a coffee; the same bug caught in prod costs you an outage and an apology. The whole point of the journey is to give problems every chance to surface before real people are affected.

## Promotion: moving the SAME artifact forward

**What it actually is.** **Promotion** is the act of taking an artifact that has proven itself in one environment and moving that *exact same artifact* to the next one. You promote `1.4.0` from staging to prod - meaning the identical, frozen `1.4.0` that passed staging is now what runs in production.

**Why people get this wrong.** The intuitive (and wrong) picture is: build for dev, then build again for staging, then build again for prod. It feels thorough. It is a trap - and it's exactly the trap Phase 2 warned about. Three separate builds are three potentially *different* artifacts. You'd be testing one thing in staging and shipping a *different* thing to prod, then wondering why prod behaves strangely.

**What promotion does in real life.** Because the artifact is immutable and lives in a registry (Phase 2), promotion is just pointing the next environment at the version that already passed:

```console
$ deploy --env staging --version 1.4.0
Pulling myapp:1.4.0 from registry...
Deployed myapp:1.4.0 to staging.

# ... 1.4.0 is verified in staging ...

$ deploy --env production --version 1.4.0
Pulling myapp:1.4.0 from registry...
Deployed myapp:1.4.0 to production.
```

*What just happened:* The exact same `myapp:1.4.0` artifact was pulled from the registry twice - once for staging, once for production. Nothing was rebuilt between the two deploys. (`deploy` here stands in for whatever your team uses; the names differ, the move is the same.) Production is now running the *byte-for-byte identical* artifact that you just watched pass in staging. That identity is the entire source of your confidence.

💡 **Key point.** Promotion means *moving an artifact*, not *rebuilding it*. "Build once (Phase 1), freeze it (Phase 2), promote that one frozen thing through environments (Phase 3)" is the spine of the whole release process. If you remember one sentence from this guide, remember that one.

## Config per environment: same code, different settings

**What it actually is.** If the artifact is identical in every environment, how does staging talk to the staging database while prod talks to the prod database? The answer: those differences don't live *inside* the artifact. They live in **configuration** that each environment supplies from the outside.

**What it does in real life.** The artifact is built to read its settings - database address, API keys, feature switches - from its surroundings rather than having them baked in. Each environment hands the same artifact a *different* set of values:

```mermaid
flowchart TD
  Artifact[the SAME artifact: myapp:1.4.0]
  DevCfg[DEV config<br/>test DB] -->|reads config| Artifact
  StgCfg[STAGING config<br/>staging DB] -->|reads config| Artifact
  ProdCfg[PROD config<br/>real DB] -->|reads config| Artifact
```

One artifact, three sets of settings. The code that runs is identical; only the values it's fed change from place to place. This is the companion idea to "build once": you can ship one artifact everywhere *because* the per-place differences have been pulled out into config. (The how-to of feeding config in safely - environment variables, secrets, and the traps around them - has its own guide: [Environment Variables & Config](/guides/env-vars-and-config).)

⚠️ **The big one: "works in staging, breaks in prod."** This is the most common gotcha in releasing, and now you can decode it. If the *same artifact* passed in staging and then breaks in prod, the artifact is not the suspect - it's identical in both. The difference is almost always the **environment or its config**: a setting that's right in staging and wrong (or missing) in prod, a database prod can't reach, a key that wasn't set, a service that exists in one place but not the other. Knowing this tells you exactly where to look first - at what *differs* between the two environments - instead of staring at code that is provably the same in both. That's the payoff of "build once, deploy everywhere": when something breaks after promotion, you've already ruled out the artifact, so the search starts in the right place.

**Why this saves you later.** The first time prod breaks and staging didn't, you won't spiral. You'll think: *same artifact, so it's a difference between the environments* - and you'll go hunting in the config, which is where the answer lives.

## Recap

1. **Environments** (dev → staging → prod) are separate, isolated copies of your running system - rehearsal stages of increasing seriousness, so mistakes happen somewhere cheap.
2. **Promotion** moves the *same* frozen artifact forward through those environments. You never rebuild per environment.
3. **The spine:** build once, freeze it, promote that one artifact - the identity of the artifact across environments is what makes you confident.
4. **Config lives outside the artifact.** One artifact reads different settings in each environment, which is *why* you can ship the same build everywhere.
5. **"Works in staging, breaks in prod" = an environment or config difference**, not a code difference - because the code (the artifact) is provably identical.

You now understand the whole journey: source becomes a built artifact, the artifact gets a version and is frozen in a registry, and that one artifact is promoted through environments with config supplied per place. The natural next question is *who pushes these buttons, and how do we make it automatic and reliable* - which is exactly what [What CI/CD Does](/guides/what-cicd-does) picks up. You might also enjoy [What Happens When Code Runs](/guides/what-happens-when-code-runs) to see what the artifact does once it's finally live.
