I use Docker so that the process that served the last kiosk order is bit-for-bit the process I tested. That sounds obvious. It is not how a lot of freelance and early-stage software still ships - Node installed by hand, `pm2 restart`, a README that lies about the Node version.
The container contract
Every API I put in production answers four questions inside the image:
- What command starts the server?
- Which port does it bind?
- Where is `/health` (liveness) and what does “ready” mean (database reachable)?
- Which environment variables are required, and which are secrets?
Cloud Run, Compose, and CI all honor that contract. I do not write a second start script for “the server.”
Multi-stage builds for TypeScript APIs
SVSFood’s backend is Fastify + TypeScript + Prisma. The Docker build has two jobs that must not share a filesystem in the final image:
- Build stage: install all dependencies, `prisma generate`, compile TypeScript.
- Runtime stage: copy `node_modules` production subset (or a bundled output), the Prisma engine, and `dist/`. Base image is Node LTS slim. User is not root.
Why the split: build tools and leftover caches are attack surface and cold-start weight. Cloud Run bills for memory and CPU on the running container. A 1.4 GB “dev” image is not professionalism. It is waste.
Local parity with Compose
On a laptop I run `docker compose` with the API image (or a bind-mount for iteration) and a Postgres 15/16 service that matches what Supabase or Cloud SQL runs. Migrations apply the same way they apply in CI. If a query needs a new index, I find out before the kiosk does.
CI, tags, and rollback
CI builds the image, runs tests against Compose, and pushes a digest to Artifact Registry (or a similar registry). Cloud Run is updated to that digest. Rollback is pointing the service at the previous digest. I do not “rebuild from memory” at 11pm.
Health checks that mean something
A `/health` that always returns 200 is a lie. Mine checks that the process is up and that it can obtain a connection from the pool. Cloud Run uses that to take an instance out of rotation. During a bad migration I would rather serve 503 from a new revision and roll back than serve 200 and corrupt orders.
What Docker does not solve
Containers do not fix a bad schema. They do not replace load testing a checkout path. They do not make WebSockets free - you still decide min instances. They do not secure secrets; Secret Manager does. Docker only removes *environment drift* from the list of ways a Friday deploy can fail.
For where those images run, and why that place is usually GCP, read GCP production architecture and the combined Docker + GCP + Supabase case study.