Warming up the neural circuits...
By the end of this chapter you will:
Deployment is the process of taking code that lives on your laptop and making it run safely on a production server where real users (and real money) depend on it. The goal: no downtime, no data loss, instant rollback if something goes wrong.
Developer pushes code
│
▼
┌─────────────────────────────────────────────────┐
│ CI: Pull Request checks │
│ 1. Lint (ESLint) │
│ 2. Type check (tsc --noEmit) │
│ 3. Unit tests (npm test) │
│ 4. Integration tests │
│ If any fail → PR is blocked │
└────────────────────┬────────────────────────────┘
│ PR merged
▼
┌─────────────────────────────────────────────────┐
│ CD: Build │
│ 5. Build Docker image │
│ 6. Tag with git SHA (e.g. image:abc1234) │
│ 7. Push to container registry │
└────────────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ CD: Staging │
│ 8. Run DB migrations on staging DB │
│ 9. Deploy new image to staging │
│ 10. Run smoke tests against staging │
│ If migrations or smoke tests fail → HALT │
└────────────────────┬────────────────────────────┘
│ Manual approval gate
▼
┌─────────────────────────────────────────────────┐
│ CD: Production │
│ 11. Run DB migrations on production DB │
│ 12. Rolling deploy (one pod at a time) │
│ 13. Verify: error rate normal, latency normal │
│ 14. If metrics spike → rollback │
└─────────────────────────────────────────────────┘packages your app, its dependencies, and its runtime into a single portable image. The image runs the same way on your laptop, on staging, and in production.
# ── Stage 1: Build ──────────────────────────────────────────────────────────
FROM node:20-alpine AS builder
WORKDIR /app
# Install dependencies first (cache-friendly: only reinstall if package.json changes)
COPY package*.json ./
RUN npm ci
# Copy source and build TypeScript
COPY . .
RUN npm run build
# ── Stage 2: Production image ────────────────────────────────────────────────
FROM node:20-alpine AS production
WORKDIR /app
Why multi-stage? Stage 1 installs everything including compilers. Stage 2 starts fresh with only what's needed to run. The final image is smaller and has fewer attack vectors.
| Rule | Why |
|---|---|
USER node (non-root) | If the app is compromised, attacker doesn't get root access to the host |
| Multi-stage build | Smaller image, fewer attack vectors |
npm ci not npm install | Installs exact versions from package-lock.json |
| Tag with git SHA | image:abc1234 tells you exactly what code is running |
| HEALTHCHECK defined | Orchestrator restarts the pod if health check fails |
| Never bake secrets into the image | Secrets go in via environment variables at runtime |
| # | Factor | What it means for this project |
|---|---|---|
| 1 | Codebase | One repo, many deploys (staging/prod use the same code, different env vars) |
| 2 | Dependencies | Explicitly declared in package.json, locked in package-lock.json |
| 3 | Config | Env vars only — no hardcoded URLs, passwords, or environment checks in code |
| 4 | Backing services | Treat Postgres, Redis, SMTP as swappable resources |
| 5 | Build/Release/Run | Three separate stages — don't build in production |
| 6 | Processes | App is stateless — no local between requests |
| 7 | Port binding | App exports its service via port (3002) |
| 8 | Concurrency | Scale by adding pods, not by making one process bigger |
| 9 | Disposability | Fast startup, graceful shutdown |
| 10 | Dev/prod parity | Development uses Docker too — same Postgres version, same Redis version |
| 11 | Logs | Write to stdout/stderr, let the platform collect and ship them |
| 12 | Admin processes | Migrations, scripts run as one-off commands against the same environment |
Migrations run before the new code is deployed. Not after.
Without this rule (bad):
10:00 — Deploy new code (expects new columns)
10:00 — App crashes (columns don't exist yet)
10:02 — Run migration
10:02 — App recovers (2 minutes of errors)With this rule (correct):
10:00 — Run migration (adds new columns — old code ignores them)
10:01 — Deploy new code (columns already exist)
10:01 — Zero errors during the entire processNew columns must be nullable (or have a DEFAULT) when first added. Your old code still runs during the deployment window and doesn't know about the new column. If the column is NOT NULL with no default, inserting rows with the old code fails.
When wants to stop a pod, it sends SIGTERM. Your app gets ~30 seconds to finish in-flight requests and clean up.
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks(); // NestJS handles SIGTERM/SIGINT
await app.listen(3002);
}NestJS's enableShutdownHooks() will:
OnModuleDestroy hooks (close DB connections, flush logs)Without this, a pod kill drops all in-flight requests immediately — users get connection reset errors.
Define what "bad" looks like before you deploy:
With rolling deploys (one pod at a time), a bad deploy only affects a fraction of traffic before you can rollback.
Migrations before code, always. If the migration fails, halt deployment. The 2 minutes you save by deploying first will cost you hours when the old code writes to a column that doesn't exist yet.