Lesson 12 of 30
Dev, staging, and production aren't just three names for 'where the app runs' — and rebuilding your app separately for each one is how 'works in staging' bugs get made.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A team usually runs its app in at least three places: dev (fast, disposable, for active development), staging (a close mirror of production, for final checks), and production (real users, real consequences). Those aren't just labels — they're meant to be a filter. Something has to survive dev, then survive staging, before it's trusted with production traffic.
The part that actually matters, and that a lot of pipelines get wrong: you should build the application once, and promote that exact same build through the environments — not rebuild it fresh for each one. Promotion means taking the literal artifact (the same .zip, the same Docker image, the same compiled bundle) that passed in staging and deploying that unchanged artifact to production. If instead each environment triggers its own separate build, you get "works in staging, broken in prod" — because a rebuild can pull a different dependency version, run against a different Node version, or simply produce different output than the one you actually tested, even from the same source commit.
name: Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: pnpm install --frozen-lockfile
- run: pnpm build
- name: Build and tag one image
run: docker build -t myapp:${{ github.sha }} .
- name: Push to registry
run: docker push myapp:${{ github.sha }}
deploy-staging:
needs: build
runs-on: ubuntu-latest
environment: staging
steps:
- name: Deploy the exact image built above
run: kubectl set image deployment/myapp app=myapp:${{ github.sha }} --context=staging
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production # GitHub Environments can require manual approval here
steps:
- name: Deploy the SAME image, unchanged
run: kubectl set image deployment/myapp app=myapp:${{ github.sha }} --context=production
Notice build runs exactly once. deploy-staging and deploy-production both reference myapp:${{ github.sha }} — the identical image tag. Production never runs pnpm build again; it just points a Kubernetes deployment at the artifact that staging already validated. GitHub's environment: key is what lets you attach environment-specific secrets and, on production, a required manual approval gate before that job runs — the artifact is the same, but the door into production has a lock the door into staging doesn't.
package.json, a slightly different base image digest — any of these can make "the same code" produce a genuinely different artifact. Promotion eliminates the entire category..env file. Config (API keys, feature flags, database connection strings) should differ by environment — the compiled code should not. Confusing "config that varies" with "code that varies" is how a rebuild-per-environment setup happens by accident.The earlier lesson on build artifacts covered producing one durable, reusable output from a build step. Promotion is the payoff: that artifact is the unit that moves through dev, staging, and production unchanged, which is exactly what makes "it passed staging" a meaningful, trustworthy statement about what's about to hit production.