Skip to content

CI/CD Fundamentals

Cloud and DevOps questions in a frontend/full-stack interview are rarely deep. They test whether you understand how your code gets from your laptop to production and what can go wrong. This page covers exactly that.


1. What Is CI/CD?

Continuous Integration (CI)

Every push runs an automated pipeline: install, lint, type-check, test, build. The point is to catch breakage within minutes of the commit, not days later during a release.

Continuous Delivery

Every change that passes CI is automatically prepared for release. Deploying to production is a manual button press.

Continuous Deployment

Every change that passes CI goes to production automatically. No button.

The Interview Answer

"CI is automatically verifying every commit. Continuous Delivery means every passing commit is deployable. Continuous Deployment means it actually ships automatically. The distinction between the last two is whether a human presses a button."

Most candidates blur Delivery and Deployment. Getting it right is a cheap, easy win.


2. Why It Matters

Without CI/CDWith CI/CD
"Works on my machine"Same build steps every time
Manual deploy checklistsOne command, or zero
Big risky releasesSmall frequent changes
Breakage found in productionBreakage found in the PR
Deploys need one specific personAnyone can ship
Rollback is a scrambleRollback is a click

The Real Argument

Small, frequent deploys are safer than big rare ones. When you ship 40 changes at once and something breaks, you have 40 suspects. When you ship one, you have one — and reverting costs nothing.


3. Pipeline Stages

A typical pipeline for a Next.js or Node app:

push / PR

  ├── 1. Checkout code
  ├── 2. Setup Node + restore dependency cache
  ├── 3. Install dependencies (npm ci)
  ├── 4. Lint (eslint)
  ├── 5. Type check (tsc --noEmit)
  ├── 6. Unit tests
  ├── 7. Build
  ├── 8. Integration / E2E tests
  ├── 9. Security scan (npm audit)

  └── on merge to main
        ├── 10. Deploy to staging
        ├── 11. Smoke tests
        └── 12. Deploy to production

Ordering Principle

Fastest and most likely to fail goes first. Lint takes 10 seconds; E2E takes 5 minutes. Failing lint after a full E2E run wastes everyone's time.

npm ci vs npm install

npm installnpm ci
Uses the lockfileAs a hintExactly
Modifies the lockfileCanNever
Existing node_modulesReusesDeletes first
Speed in CISlowerFaster

Always npm ci in CI. It guarantees the exact dependency tree from the lockfile — otherwise a transitive dependency can silently update between your local run and the pipeline. This is a very common interview question.


4. GitHub Actions

The most common CI system, and the one to know.

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm test
      - run: npm run build

Key Concepts

TermMeaning
WorkflowA YAML file in .github/workflows/
EventWhat triggers it — push, pull_request, schedule, workflow_dispatch
JobA group of steps on one runner. Jobs run in parallel by default
StepOne command or action
ActionA reusable step (actions/checkout@v4)
RunnerThe VM executing the job
SecretEncrypted value from repository settings, never in the YAML

Jobs With Dependencies

yaml
jobs:
  test:
    runs-on: ubuntu-latest
    steps: [...]

  deploy:
    needs: test                                    # only if test passed
    if: github.ref == 'refs/heads/main'            # only on main
    runs-on: ubuntu-latest
    environment: production                        # can require approval
    steps:
      - run: ./deploy.sh
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

needs creates the dependency; without it jobs run in parallel.

Matrix Builds

yaml
strategy:
  matrix:
    node-version: [20, 22]

Runs the job once per value — the standard way to test across versions.

Services (a Real Database in CI)

yaml
services:
  postgres:
    image: postgres:16
    env:
      POSTGRES_PASSWORD: test
    options: >-
      --health-cmd pg_isready
      --health-interval 10s
      --health-retries 5
    ports: ['5432:5432']

Integration tests against a real Postgres, not a mock. Mocking the database tests your mock, not your SQL.


5. Secrets Management

The Rules

  1. Never commit secrets. .env in .gitignore, always
  2. Store them in the platform: GitHub Secrets, Vercel env vars, Render env groups
  3. Different secrets per environment — a staging leak must not compromise production
  4. Rotate on any suspicion, and on staff departure
  5. Never log them — configure redaction at the logger

If a Secret Is Committed

Removing it in the next commit is not enough — it stays in git history and in every clone and fork.

The correct response:

  1. Rotate the secret immediately. This is step one, not step three
  2. Then purge history (git filter-repo or BFG) and force push
  3. Assume it was scraped — public repos are scanned by bots within minutes

Saying "rotate first, clean history second" is the answer that shows you have actually dealt with this.

Build-Time vs Runtime

Frontend variables (NEXT_PUBLIC_*, VITE_*) are inlined into the bundle at build time. They are permanently public and require a rebuild to change — updating them in a dashboard does nothing to an already-built app.

Backend variables are read at runtime and a restart picks them up.

Never put a secret in a NEXT_PUBLIC_ variable. It ships in the JavaScript bundle, readable by anyone with DevTools. This is the single most common real-world deployment mistake.


6. Environments

EnvironmentPurpose
LocalYour machine
PreviewOne per pull request, auto-created and destroyed
StagingProduction-like, shared, for final verification
ProductionReal users

Preview Deployments

Vercel, Netlify and Render create a unique URL for every PR. Reviewers click a link instead of pulling the branch and running it locally.

This is the highest-value CI/CD feature for frontend teams and worth naming as a favourite.

Environment Parity

Every environment should run the same build artifact with different configuration. Rebuilding per environment means what you tested is not what you shipped.

The NEXT_PUBLIC_ build-time inlining above is exactly why perfect parity is hard for frontend apps — it's a good nuance to raise.


7. Deployment Strategies

StrategyHowTrade-off
RecreateStop old, start newDowntime. Simple
RollingReplace instances graduallyNo downtime; two versions coexist briefly
Blue-GreenTwo full environments, switch trafficInstant rollback; costs 2× infrastructure
Canary5% of traffic to the new version, then increaseSafest; needs good metrics
Feature flagsShip code disabled, enable per userDecouples deploy from release

The Nuance Worth Adding

Deploying is not releasing. With feature flags you ship code to production continuously and turn features on separately — so a bad feature is a config toggle away from being off, not a rollback and redeploy.

Rolling Deploys Need Two Things

  1. Graceful shutdown — handle SIGTERM, stop accepting new connections, finish in-flight requests
  2. Backwards-compatible database migrations — during a rolling deploy, old and new code run against the same schema at the same time

Backwards-Compatible Migrations

❌ One deploy:  ALTER TABLE users RENAME email TO email_address;
                Old instances still query `email` → 500s during the rollout

✅ Three deploys:
   1. Add email_address, write to both columns
   2. Backfill, switch reads to the new column
   3. Drop the old column

Called expand and contract. It is the answer to "how do you deploy a schema change with zero downtime" and it separates people who have done it from people who haven't.


8. Rollback

Every deploy needs a plan for when it goes wrong.

MethodSpeed
Platform "promote previous deployment" (Vercel, Render)Seconds
Redeploy the previous git tagMinutes
git revert and pushMinutes, plus a full CI run
Feature flag offInstant

What Does Not Roll Back

Database migrations. Code rolls back in seconds; a dropped column does not come back. This is why destructive migrations should be a separate, later deploy — after the new code has been running successfully.

git revert is preferable to git reset --force on a shared branch: it adds a new commit rather than rewriting history everyone else has pulled.


9. Monitoring After Deploy

A deploy is not done when the pipeline goes green.

WatchTool
Error rateSentry, Datadog
Latency (p95, p99)Platform metrics, Datadog
UptimeBetter Uptime, Pingdom
Core Web VitalsVercel Speed Insights, Lighthouse CI
LogsPlatform logs, Logtail, Datadog

Health Endpoints

js
app.get("/health", (req, res) => res.json({ status: "ok" }));   // liveness

app.get("/ready", async (req, res) => {                          // readiness
  try {
    await db.$queryRaw`SELECT 1`;
    res.json({ status: "ready" });
  } catch {
    res.status(503).json({ status: "not ready" });
  }
});

Liveness = is the process alive (restart me if not). Readiness = can I serve traffic (stop routing to me if not). Confusing them causes restart loops — a database blip restarts a perfectly healthy process.


10. Common Interview Questions

"Walk me through your deployment process."

"Push to a feature branch. CI runs lint, type check, tests and a build on the PR, and the platform creates a preview deployment on a unique URL. After review and a green pipeline, merge to main. That triggers a production deploy — build, then a rolling release with health checks. I watch the error rate and latency for a few minutes. If something's wrong, I promote the previous deployment, which takes seconds."

"What's the difference between CI and CD?"

CI verifies every commit automatically. Continuous Delivery keeps every passing commit deployable. Continuous Deployment ships it automatically. Delivery vs Deployment = whether a human presses a button.

"How do you handle environment variables?"

Never in git. Stored per environment in the platform. Frontend NEXT_PUBLIC_/VITE_ variables are build-time inlined and permanently public — never secrets, and changing one needs a rebuild. Backend variables are runtime and picked up on restart.

"How do you do zero-downtime deploys?"

Rolling deploys with health checks, graceful shutdown handling SIGTERM, and backwards-compatible migrations using expand-and-contract so old and new code can run against the same schema simultaneously.

"A deploy broke production. What do you do?"

  1. Roll back first, diagnose second. Stop the bleeding
  2. Promote the previous deployment, or flip the feature flag off
  3. Confirm error rates recover
  4. Reproduce locally or in staging
  5. Fix, add a test that would have caught it, redeploy
  6. Blameless postmortem — what in the pipeline let this through?

The "roll back first" instinct is the thing being tested. Candidates who start debugging production while it's down give the wrong answer.

"What's the difference between npm install and npm ci?"

npm ci installs exactly what the lockfile specifies, deletes node_modules first, never modifies the lockfile, and is faster. npm install can resolve to newer versions and update the lockfile. Always ci in a pipeline — otherwise CI can pass on a different dependency tree than the one you tested.

"How do you keep the pipeline fast?"

  • Cache dependencies (cache: npm in setup-node)
  • Run independent jobs in parallel
  • Fail fast — cheapest checks first
  • Only run E2E on PRs to main, not every push
  • Run tests affected by the change (Turborepo, Nx) in a monorepo
  • Cache Docker layers

A slow pipeline gets bypassed, and a bypassed pipeline protects nothing.

© 2025 DDocs · Dipak's Documentation Guide