Interview bookGroundworkInterview book
15 / 27 written chapters

R9 · Part 2 · The technical rounds

AWS, Docker & CI/CD

Your AWS surface is narrow — EC2, S3 and Route 53. That is completely fine at six years for a full-stack role. What is not fine is letting them discover it. Own the boundary before they find it and it becomes honesty; let them find it and it becomes a gap.

8 questions28 follow-ups8 traps13 min read
Length
20–30 min, usually folded in
Who
Backend lead or DevOps
Decides
Whether your deployment claims are real
Fail mode
Overclaiming cloud depth
Asked at
  • product
  • saas
  • service
  • agency (less often)
Practise this round as a mock →

Everything open, top to bottom.

09On this page
  1. Describe your deployment pipeline end to end.
  2. If you have cut infrastructure cost, how exactly did you do it?
  3. Walk me through a Dockerfile for a Node service. What makes it good?
  4. How do you manage secrets and configuration across several environments?
  5. Rapid-fire infrastructure
  6. Your Docker build takes eight minutes on every commit. Make it fast.
  7. Design the CI pipeline for a team of eight. What runs, in what order, and what blocks a merge?
  8. How do you deploy with zero downtime — and what should the health check actually check?
  9. All you have in production is logs. What would you add first, and in what order?
R9.1

Describe your deployment pipeline end to end.

What they are really testingWhether the deploy-time number on your resume is a real pipeline you built or a line you wrote.

That last paragraph is worth more than pretending. Every interviewer has been burned by a candidate who listed Kubernetes and could not explain a pod — and they test for it, so the downside of overclaiming is severe and the upside of honesty is real.

Say it like this

A push to a branch triggers a GitHub Actions workflow: install, lint, type-check, unit tests, then a Docker build. On merge to main it builds the image, tags it with the commit SHA, pushes to the registry, and deploys to EC2 by pulling the new tag and restarting the container behind a health check. That took our release from a roughly three-hour manual process — SSH in, pull, install, build, restart, check by hand — to about twenty minutes, which is the difference between a hotfix being an event you schedule and a hotfix being routine.

To be straight about the shape of it: this is Docker on EC2 instances, not a managed orchestrator. We did not run Kubernetes or ECS, because at three applications and this traffic the operational cost was not worth what it would have bought us. I can hold a conversation about ECS task definitions, but I would be learning it properly on the job rather than claiming it.

The answer that loses the room

Deploying the latest tag. Nobody can say which commit is running, two instances that pulled at different times can run different code, and a rollback means guessing which image was "latest" yesterday.

They will push with
  1. How do you roll back?
  2. What runs in CI that is not a test?
  3. Why tag with the SHA rather than latest?
How did you do?
R9.2

If you have cut infrastructure cost, how exactly did you do it?

What they are really testingThe metric interrogation again, in the infrastructure round.

Have the specific levers ready, and name which one accounted for most of it. The credible ones for your setup:

  • Right-sizing. Instance sizes chosen at project start and never revisited against actual CloudWatch utilisation. This is usually the biggest single win and it is the most honest answer.
  • Non-production environments. Three non-production environments running production-sized instances 24/7. Shrink them, and shut them down outside working hours — that alone is roughly a 65% saving on those instances.
  • S3 lifecycle rules moving old assets to infrequent-access or Glacier.
  • Orphaned resources — unattached EBS volumes, idle Elastic IPs, old snapshots, unused load balancers. Every long-running AWS account has these.
  • Savings plans or reserved instances for the steady-state baseline, on-demand for the rest.
  • Data transfer — often the invisible line item. Serving assets from S3 and CloudFront instead of from the instance.
Say it like this

Mostly it was right-sizing and the non-production environments. We had picked instance sizes at the start of the project and never revisited them against real utilisation, and the three non-production environments were running the same sizes as production around the clock.

The answer that loses the room

A percentage with no absolute figure behind it. Twenty percent of which monthly bill, compared across which months? Without the before and after in rupees or dollars, it sounds like a number chosen for the resume.

They will push with
  1. How did you measure the baseline?
  2. What did you look at to decide a size was wrong?
  3. What would you cut next?
How did you do?
R9.3

Walk me through a Dockerfile for a Node service. What makes it good?

What they are really testingWhether you copied a Dockerfile or understand layer caching and image size.

Score the points explicitly as you go:

  • Multi-stage, so build tooling and dev dependencies never ship. Often a 10× size difference.
  • Copy the manifest before the source, so the npm ci layer is reused when only code changes. This is the single biggest build-time win and the thing most Dockerfiles get wrong.
  • npm ci not npm install — reproducible from the lockfile, and it fails rather than silently updating.
  • Non-root user.
  • Exec-form CMD. Shell form wraps the process in /bin/sh, which does not forward SIGTERM — so your graceful shutdown from R5 never runs and every deploy kills in-flight requests.
  • .dockerignore with node_modules, .git, .env — otherwise you copy the host's node_modules into the build context and both slow the build and risk shipping secrets.
# ---- build ----
FROM node:24-alpine AS build
WORKDIR /app
COPY package*.json ./          # before the source: this layer caches
RUN npm ci
COPY . .
RUN npm run build

# ---- run ----
FROM node:24-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build /app/dist ./dist
USER node                      # never run as root
EXPOSE 3000
HEALTHCHECK --interval=30s CMD node healthcheck.js
CMD ["node", "dist/main.js"]   # exec form — receives SIGTERM directly
The answer that loses the room

COPY . . above RUN npm ci, with no .dockerignore. Every commit reinstalls every dependency, and the host's node_modules — possibly built for a different operating system — gets copied over the ones installed in the image.

They will push with
  1. Why is the image still 400MB and how would you shrink it? (distroless, or alpine + prune.)
  2. What is a layer and why does order matter?
  3. How do you get secrets into a build without baking them in?
How did you do?
R9.4

How do you manage secrets and configuration across several environments?

What they are really testingIf your resume claims several environments, this is the audit.

Nothing in the repository, ever. Then a layered answer:

  • Pipeline secrets — GitHub Actions encrypted secrets or environments with required reviewers for production.
  • Runtime secrets — AWS Systems Manager Parameter Store (cheap) or Secrets Manager (rotation built in), read at boot or injected by the deploy.
  • IAM roles, not long-lived access keys. An instance profile or OIDC federation from GitHub Actions means there is no static key to leak.
  • A committed .env.example documenting required keys without values, so a new developer knows what is needed.
  • Boot-time validation — ConfigModule with a Joi or Zod schema, so a missing variable fails the process immediately with a clear message rather than causing a null-pointer at 3am inside one request path.
The answer that loses the room

Passing secrets as build arguments or baking them in with ENV. Both are readable by anyone who can pull the image — from its history or its config — so the registry quietly becomes a secrets store nobody meant to create.

If they push on what you would do differently: per-environment parameter paths (/app/prod/DB_URL) with IAM policies scoped to the path, so the staging role literally cannot read production secrets. That is the control that turns a mistake into a permission error.

They will push with
  1. What do you do when a secret leaks?
  2. How do you rotate a database password with zero downtime?
  3. How does a developer run this locally?
How did you do?
R9.5

Rapid-fire infrastructure

What they are really testingBreadth. Answer honestly — this is the round where overclaiming is most likely to be caught.

  • Image vs container vs layer. An image is a read-only template built from stacked layers; a container is a running instance of one with a writable layer on top.
  • docker-compose in development, not production. It has no rolling updates, no health-based routing, no multi-host scheduling. Fine for local dependencies; not a deployment strategy.
  • Blue-green vs rolling vs canary. Blue-green: two full environments, switch traffic, instant rollback, double the cost. Rolling: replace instances gradually, cheap, but both versions run at once so your database schema must support both. Canary: a small percentage first, watch the error rate, then proceed.
  • Rollback in under five minutes. Because images are tagged by commit SHA, rollback is re-deploying the previous tag. The thing that makes rollback hard is a database migration that already ran — which is why expand-migrate-contract from R6 matters.
  • EC2 vs ECS vs Lambda. EC2 when you want full control and a steady load. ECS/Fargate when you want containers without managing the hosts — the natural next step from where you are. Lambda for spiky, short, stateless work; watch cold starts and the 15-minute limit.
  • What a load balancer buys you. Health-checked routing, TLS termination, zero-downtime deploys, and a place to put WAF rules. Without one you cannot deploy without dropping requests.
  • S3 + CloudFront over serving from Node. Your process should never spend its single thread pushing static bytes. The CDN also puts the asset physically closer to the user.
  • IAM role vs user. A user has long-lived credentials; a role is assumed temporarily and its credentials rotate automatically. Prefer roles everywhere — a leaked role session expires, a leaked access key does not.
  • Monitoring — be honest. "Today it is structured logs, and I know that is the gap. What I would add first is error tracking (Sentry), then p95 latency and error rate per endpoint with alerts, then an uptime check. I would add tracing last, because it costs the most to instrument and pays off only once you have several services."
  • Have you been on call? Answer truthfully. If not: "not on a formal rota — I have been the person called when a client system broke, which is the same job without the pager." Then describe an actual incident.
  • Zero-downtime deploy with a migration in it. The expand-migrate-contract sequence, plus: migrations run as a separate step before the new code deploys, never on application boot — otherwise five instances start migrating simultaneously.
  • Infrastructure as code. If you have not used Terraform, say so, and say what you did instead (console plus documented steps) and why that is worse — drift, no review, no reproducibility.
R9.6

Your Docker build takes eight minutes on every commit. Make it fast.

What they are really testingWhether you understand layer caching well enough to predict which lines rebuild, and whether your CI keeps a cache at all.

Find out which step is slow and why it is not cached. Nine times in ten it is one of three things:

  1. The dependency layer is invalidated by every commit, because COPY . . sits above RUN npm ci. Any change to any file busts the cache and every dependency reinstalls. Copy the manifest and lockfile, install, then copy the source.
  2. The build context is huge. No .dockerignore, so node_modules, .git and old build output are sent to the builder every time — and a change to any of them invalidates the COPY.
  3. CI starts from an empty cache. A fresh runner has no layers, so a perfectly ordered Dockerfile still builds from scratch. The cache has to be exported and imported explicitly.

Then two BuildKit features most teams have not turned on. A cache mount keeps the npm cache between builds without putting it in the image. A secret mount hands a build step a token — for a private registry, say — that never lands in a layer.

# syntax=docker/dockerfile:1
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm ci

FROM deps AS build
COPY . .
RUN npm run build
docker buildx build \
  --cache-from type=gha \
  --cache-to type=gha,mode=max \
  --secret id=npmrc,src=$HOME/.npmrc \
  -t app:$GIT_SHA .
The answer that loses the room

Passing a registry token with ARG NPM_TOKEN or ENV to make npm ci work. Build arguments show up in the image history and environment variables are baked into the image config, so anyone who can pull it can read the token. Secret mounts exist for exactly this.

On GitHub Actions, docker/build-push-action wires up the type=gha cache for you; on any other CI, type=registry stores the cache as an image next to your app image. Either way, the number to quote is the second build after a one-line code change — if that still reinstalls dependencies, the ordering is wrong.

And the honest ceiling: once caching works, what is left is usually the TypeScript build and the test suite. That is a CI problem, not a Docker one, and the fix is running them as parallel jobs rather than inside the image build.

They will push with
  1. Which line of your Dockerfile invalidates the cache most often?
  2. How do you get a private package token into the build safely?
  3. Why measure the second build after a code change rather than the first?
  4. Would you build the image once and promote it, or rebuild it per environment?
How did you do?
R9.7

Design the CI pipeline for a team of eight. What runs, in what order, and what blocks a merge?

What they are really testingWhether you design CI for fast feedback and trust, rather than as a list of tools.

Two goals that pull in different directions: a developer should know within a few minutes if they broke something, and nothing untested should reach production. The design is the ordering that gets you both.

  • Fast checks first, in parallel. Lint, type-check and unit tests as separate jobs running at the same time. A lint failure should come back in a minute, not after a ten-minute build.
  • Then the slow checks, only once the cheap ones pass: integration tests against a real Postgres in a service container, the Docker build, and an end-to-end smoke test on the flows that take money.
  • Required checks and branch protection. The merge button stays disabled until the required jobs pass and a reviewer approves. That is what turns CI from a suggestion into a rule.
  • Build once, promote the same artifact. The image tested on main is the image that goes to staging and then production, tagged by commit SHA. Rebuilding per environment means production runs something nobody tested.
  • Cancel superseded runs. Three pushes to one branch should not run three full pipelines.
on: [pull_request]

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true            # a new push cancels the old run

jobs:
  lint:       { runs-on: ubuntu-latest, steps: [ ... ] }
  typecheck:  { runs-on: ubuntu-latest, steps: [ ... ] }
  unit:       { runs-on: ubuntu-latest, steps: [ ... ] }
  integration:
    needs: [lint, typecheck, unit]    # only after the fast checks pass
    runs-on: ubuntu-latest
    services:
      postgres: { image: postgres:17, env: { POSTGRES_PASSWORD: test } }
    steps: [ ... ]
The answer that loses the room

One long sequential job that installs, lints, tests, builds and deploys in a single script. It works, it takes twenty minutes, it fails on the last step as often as the first, and nobody can tell from the red cross what actually broke.

Then the parts that decide whether the team trusts it. A flaky test is an incident: one that fails one run in twenty trains everyone to press re-run, and the day it catches something real nobody believes it — quarantine it and fix it that week. Keep the whole pipeline to roughly ten minutes, because beyond that people stop waiting for it and merge on hope. And security checks belong here — a dependency audit and secret scanning on every pull request, because the cheapest place to catch a committed key is before it merges.

They will push with
  1. A test fails one run in twenty. What do you do?
  2. How do you keep the pipeline fast as the codebase grows?
  3. Where do database migrations run in this pipeline?
  4. What changes for a monorepo with four apps in it?
How did you do?
R9.8

How do you deploy with zero downtime — and what should the health check actually check?

What they are really testingWhether you know the three moments a deploy drops requests, and whether your health check could take the whole fleet down.

A deploy drops requests at one of three moments, and a zero-downtime deploy is closing all three:

  1. Traffic reaches the new instance before it is ready — the process is up but still connecting to the database or warming a cache. Fix: a readiness check, and the load balancer only routes to instances that pass it.
  2. Traffic reaches the old instance after it has started shutting down. Fix: on SIGTERM, fail readiness first, wait for the load balancer to notice, then stop accepting connections and drain the in-flight ones — the SIGTERM answer from R5. The load balancer's deregistration delay must cover your drain time.
  3. Old and new code disagree about the database. During a rolling deploy both versions run at once, so every schema change has to work with both — expand, migrate, contract, from R6.

Then roll gradually: replace a few instances, watch error rate and latency, continue. If the numbers move, stop and redeploy the previous image tag.

// liveness: is this process wedged? restart it if so
app.get('/live', (_, res) => res.status(200).end())

// readiness: should this instance get traffic right now?
let started = false        // set once config, pool and caches are ready
app.get('/ready', (_, res) =>
  res.status(started && !shuttingDown ? 200 : 503).end())
The answer that loses the room

A liveness probe that queries the database. It looks thorough, and it turns every short database blip into every instance being killed and cold-started at the same moment — the outage you were trying to prevent, caused by the check that was meant to catch it.

The distinction worth saying out loud: liveness asks whether the process is stuck; readiness asks whether it should get traffic. Neither should lean on shared dependencies. If every instance checks the database and the database blinks for thirty seconds, liveness restarts the whole fleet at once, or readiness pulls every instance out of the load balancer together — either way a small hiccup becomes a full outage. Check what is specific to this instance: started, configured, not shutting down.

They will push with
  1. What is the difference between liveness and readiness?
  2. How long should the drain be, and what decides it?
  3. The new version passes health checks and errors on real traffic. How do you catch that?
  4. Blue-green or rolling for this service — which, and why?
How did you do?
R9.9

All you have in production is logs. What would you add first, and in what order?

What they are really testingWhether you build observability in order of value per hour of effort, rather than naming every tool you have heard of.

Order by what each piece tells you and what it costs to add:

  1. Make the logs useful. Structured JSON, one line per event, a request id on every line, a level, and no secrets or personal data. It costs an afternoon and makes everything after it better, because you can finally follow one request through the noise.
  2. Error tracking. Sentry or an equivalent: every unhandled error grouped, counted, with a stack trace and the release it started in. This is how you stop hearing about bugs from users.
  3. The RED metrics per endpoint — rate, errors, and duration as a histogram so you get p95 and p99 — plus event loop lag and pool usage for a Node service. Then alerts on what users feel: error rate and p95 latency on the endpoints that matter.
  4. Tracing last. OpenTelemetry spans across services show where a slow request spent its time. It is the most powerful piece and the most work, and it pays off once there is more than one service in the path.
{"level":"error","time":"2026-09-26T10:42:07.118Z","reqId":"01J8X…",
 "route":"POST /bookings","status":502,"durationMs":2140,
 "err":{"type":"AppError","code":"PAYMENT_FAILED","cause":"ETIMEDOUT"},
 "msg":"payment provider timed out"}
The answer that loses the room

"We would add Prometheus, Grafana, Jaeger, ELK and PagerDuty." A list of tools with no order and no reason reads as someone who has seen the diagram, not someone who has been paged. Say what question each piece answers, and the order you would add them in.

Two sentences that land. Alert on symptoms, not causes: page someone when checkout errors pass one percent, not when CPU passes eighty, because busy CPU with happy users is not an incident. And every alert must be actionable — an alert nobody acts on is noise, and noise is how the real one gets ignored at 2am.

They will push with
  1. What would you alert on for a booking service, and at what threshold?
  2. How do you keep personal data out of the logs?
  3. Why a histogram rather than an average?
  4. A user says checkout failed at 10:42. How do you find their request?
How did you do?

Finished this round?

0 of 27 rounds read in this book