1. Courses
  2. System Design
  3. Running Software in Production
  4. Stop 3

40 minIntermediate

Shipping safely: containers, CI/CD and releases

Most outages start with a change: a new release, a migration, a config edit. Teams that ship often and safely have a pipeline that tests every change the same way, a release that reaches a few users first, and a rollback that takes one command. This lesson builds that pipeline for a NestJS and Prisma API and covers mobile releases too.

By the end you can

  • Write a small, non-root, multi-stage Docker image for NestJS and Prisma
  • Build a GitHub Actions pipeline with test, image, staging and approved production stages
  • Choose between rolling, blue-green and canary releases, and run a blue-green switch
  • Order migrations and code so any release can be rolled back
  • Use feature flags and a minimum-version check to control web and mobile releases

The everyday picture

When a grocery chain launches a new product, it does not send it to every store on day one. Quality control checks each batch at the factory. The product goes to five pilot stores for a week. Managers watch sales and complaints. If it does well, it rolls out area by area. If a batch is bad, there is a recall plan: which stores have it, how to pull it from shelves today.

Software releases work the same way. The factory checks are CI: every change is built and tested automatically. The sealed, labelled carton is the container image: the exact same thing goes to every store. The pilot stores are a canary release. The recall plan is the rollback. The team that skips the pilot and has no recall plan will one day ship a bad batch to all stores at once, on the evening before a festival rush.

Containers: build once, run the same everywhere

A holds your compiled app, the exact Node version, the node_modules and the start command. You build it once per commit, give it a tag such as the git commit id, and run that same image in staging and production. "It worked on my laptop" stops being a problem, because staging and production run the identical bytes.

Three habits make images good:

  • Multi-stage builds. A compiles in a first stage that has TypeScript and all dev packages, then copies only dist and the production node_modules into a clean final stage. Smaller images download faster to servers and contain fewer packages with security holes.
  • Non-root. The final stage runs as an ordinary user. If an attacker breaks into the process, they are not root inside the container.
  • Config from the environment. The image has no secrets and no environment-specific values. DATABASE_URL, JWT_SECRET and the rest come from the environment at start (see Scaling up vs scaling out and Security essentials). One image, many environments.

Prisma adds one rule. prisma generate runs while building the image, because it produces code and an engine file for that exact operating system. prisma migrate deploy runs at release time, once, against the real database. Never run migrations inside docker build: the build machine should not even be able to reach the production database. And do not run them in every container's start command: three copies starting together would all try to migrate at once.

Quick check

CI built the image ops-api:3f9c2e1, and it passed every test in staging. What should run in production?

Loading the rest of the lesson…