Deployment Without Mystery
Stage 3 · DevOps, Deployment & Operations · B.U.I.L.D. letter: D
You clicked a button and your app appeared on the internet. That's genuinely amazing — and also completely opaque. This lesson tears the curtain off so you know exactly what happened, can reproduce it reliably, and can undo it when something goes wrong. Deployment stops being a prayer. It becomes a pipeline.
⚠️ The vibe trap
You dragged a folder onto Netlify or hit "Deploy" on Vercel and it worked — amazing. But now you don't know what steps ran, whether the build output is the same as your dev server, or how to get back to the previous version if users start reporting a white screen. "It deployed" and "it deployed correctly and reproducibly" are two very different things. Once real users depend on your app, you need to own the deploy process, not just trust it blindly.
🔭 What actually happens on deploy
Every deploy, no matter the platform, runs the same four-stage journey:
SOURCE CODE
│
▼
[BUILD STEP] ← transpile TypeScript, bundle JS, inline env vars, tree-shake
│
▼
ARTIFACT ← a folder of static files, a compiled binary, or a Docker image
│
▼
[DISTRIBUTE] ← upload artifact to CDN nodes, push image to a registry
│
▼
RUNNING SERVER / EDGE NODES
│
▼
TRAFFIC ROUTED to the new version (old version stays alive briefly → zero-downtime)
Mental model: your source code never runs directly in production. What runs is the artifact — the output of the build step. Think of it like baking: the recipe (source) and the loaf (artifact) are different things.
Why this matters: if your build is broken, no artifact is produced and nothing ships. That's a feature, not a bug. A bad deploy that never reaches users is infinitely better than one that does.
Common mistake: editing files directly on the server ("hotfix in prod"). The next deploy overwrites your edit and it disappears. Always go through source → build → artifact.
⚙️ The build step in depth
The build step transforms your source into something browsers and servers can execute efficiently. For a typical React/Next.js app:
# What Vercel (or your CI) runs under the hood
npm ci # reproducible install — no package-lock drift
npm run build # e.g. `next build` or `vite build`
# What next build actually does:
# 1. Type-checks (if you have tsc configured)
# 2. Compiles JSX/TSX → plain JS
# 3. Bundles with Webpack/Turbopack — tree-shakes dead code
# 4. Splits code into chunks for lazy loading
# 5. Inlines NEXT_PUBLIC_* env vars as literal strings
# 6. Writes .next/ (the artifact)
Mental model: the dev server (npm run dev) is a live interpreter — it recompiles on every save and skips optimizations. The prod build is a one-time compilation that strips dev warnings, minifies, and inlines values. They can behave differently. Always test with npm run build && npm start before you call something "done."
Why this matters: a bug that only appears in the prod build (e.g., an env var that exists on your laptop but not in CI) will silently break your app for users while working fine locally.
Common mistake: running npm run dev to "test" a production deploy. Use npm run build && npm run start locally at least once per feature. Better yet, let CI do it.
🏗️ Build time vs. runtime — the hard line
| Build time | Runtime | |
|---|---|---|
| When | Before the app starts | While the app is serving requests |
| Who | CI machine / Vercel builder | Your server process or Edge worker |
| Env vars | Baked into the bundle as literals | Read from process.env on each request |
| Example | NEXT_PUBLIC_API_URL | DATABASE_URL, JWT_SECRET |
| Mistake | Putting secrets in NEXT_PUBLIC_* | Forgetting to set server env vars → crash on boot |
# SAFE: public URL baked at build time (visible in browser JS — never put secrets here)
NEXT_PUBLIC_API_URL=https://api.yourapp.com
# SAFE: secret only available at runtime on the server
DATABASE_URL=postgres://user:secret@host/db # never in the bundle
JWT_SECRET=supersecretvalue # never in the bundle
Mental model: build-time vars are frozen in amber the moment the artifact is created. Runtime vars are injected fresh every time the server process starts. The boundary between them is the edge of the artifact.
Why this matters: a secret accidentally embedded in a public bundle is exposed to every user who opens devtools. No rotation fixes it until you rebuild and redeploy.
Common mistake: putting STRIPE_SECRET_KEY in a NEXT_PUBLIC_* env var because "it worked in dev." It will ship in your JS bundle to every browser.
🚀 What deploy platforms do for you
Vercel, Render, Fly.io, and Railway are deployment platforms. They automate the boring parts of the pipeline:
┌─────────────────────────────────────────────────────────────────────┐
│ PLATFORM RESPONSIBILITIES │
│ │
│ Git push detected │
│ │ │
│ ▼ │
│ Pull source → install deps (npm ci) → run build command │
│ │ │
│ ▼ │
│ Store artifact (CDN or container registry) │
│ │ │
│ ▼ │
│ Health check new instance → route traffic if healthy │
│ │ │
│ ▼ │
│ Keep previous artifact for instant rollback │
│ │ │
│ ▼ │
│ Tail logs · alert on crash loops · auto-restart on failure │
└─────────────────────────────────────────────────────────────────────┘
Mental model: the platform is a trustworthy robot that runs the same deploy recipe every time, on a clean machine, without your local config interfering. You bring the recipe (build command, env vars, health check path). The robot runs it.
Why this matters: if you deploy from your laptop you risk "works on my machine" syndrome — different Node version, locally cached modules, env vars you forgot to document. The platform enforces a clean environment.
Common mistake: using npm install instead of npm ci in your build command. npm install can silently update packages; npm ci is reproducible.
🔄 Zero-downtime deploys and rollback
You never want a 30-second window where your site returns 502 while the new version boots. Two patterns prevent this:
Rolling deploy (Render, Fly.io defaults): spin up new instances one at a time, drain old ones only after new ones pass health checks. Traffic always has somewhere to go.
Blue-green deploy (Vercel's immutable previews, AWS Elastic Beanstalk): maintain two identical environments. New version ("green") boots fully, health checks pass, then traffic is atomically switched from "blue" to "green." Rollback is a single DNS/router switch.
# Vercel: every push gets an immutable preview URL — blue-green for free
# Rolling back on Vercel CLI
vercel rollback # rolls back to the previous production deployment
# Fly.io: rolling deploy with health check
fly deploy # default rolling strategy
fly deploy --strategy=bluegreen # full blue-green
# Render: manual rollback via dashboard
# Dashboard → Service → Deploys → click any previous deploy → "Rollback to this deploy"
# Confirming your rollback worked
curl -s -o /dev/null -w "%{http_code}" https://yourapp.com/health
# should return 200
Mental model: a rollback is just "deploy the previous artifact." That's only possible if you kept it. Every modern platform keeps the last N deploys. Never delete old releases manually.
Why this matters: at 2 AM when your new release breaks checkout, you want rollback to take 30 seconds, not 30 minutes.
Common mistake: not having a health check endpoint, so the platform can't tell whether the new version actually booted correctly. It routes traffic anyway and users hit a broken app.
❤️ Health checks
A health check is a dedicated HTTP endpoint your app exposes so the platform can verify it's alive and ready to serve traffic before routing users to it.
# Minimal health check in Next.js — create src/app/api/health/route.ts
# (or pages/api/health.ts)
#
# GET /api/health → 200 { status: "ok", ts: "2026-06-03T..." }
#
# Configure in your platform:
# Vercel: automatic (checks / by default)
# Fly.io: fly.toml → [http_service] → health_checks
# Render: Service Settings → Health Check Path → /api/health
# Railway: Settings → Health Check Path → /api/health
# Fly.io fly.toml snippet:
# [[http_service.checks]]
# grace_period = "10s"
# interval = "15s"
# method = "GET"
# path = "/api/health"
# timeout = "5s"
Mental model: the health check is the platform asking "are you ready?" before it sends a single user to the new instance. Your app must answer 200 for traffic to switch. If it answers anything else, the deploy is aborted and the old version keeps running.
Why this matters: without a health check, a server that boots but immediately crashes on the first DB query looks "healthy" to the platform until users start screaming.
Common mistake: a health check that queries the database and fails on cold start because the DB connection pool isn't warm yet. Keep health checks lightweight — just return 200 to prove the process is alive. Separate "liveness" (alive) from "readiness" (DB connected) if your platform supports it.
🛠️ Your mission
Deploy your app through a real build pipeline and practice a rollback.
- Pick one of your existing projects that has a
package.jsonwith abuildscript. - Connect it to Vercel, Render, or Fly.io via their GitHub integration (not a drag-and-drop).
- Verify the deploy log shows
npm ciandnpm run buildrunning — not just a file sync. - Add a
/api/healthroute that returns{ status: "ok" }with a 200 status code. Configure it as the health check path in your platform's settings. - Make a small intentional change (update a heading), push to main, and watch the deploy pipeline run in the platform dashboard.
- Roll back to the previous deployment using your platform's rollback button or
vercel rollback. - Confirm the rollback worked by visiting your live URL and verifying the old heading is back.
- Fill in the Production-Readiness Checklist for this project.
✅ You're done when…
- You have completed and can check off every item on your Production-Readiness Checklist
- Your deploy log shows
npm ci+npm run buildrunning on a clean CI machine (not your laptop) - Your app has a
/api/healthendpoint returning 200, configured as the platform health check path - You have successfully triggered a rollback and confirmed the previous version went live
- You can explain the difference between build-time and runtime environment variables to someone else out loud
➡️ Next: Config & Secrets in Production. Build It Right, Or Don't Build It At All. 🏛️