Containers
Date: 2026-08-17
Packaging an application with its dependencies so it runs identically everywhere. Excellent for CI, testing and production; frequently a poor trade for local front-end development, where the file-watching performance cost is real.
A container runs a process in an isolated environment with its own filesystem, network and process space, using the host kernel rather than a full virtual machine.
VM its own OS kernel
→ GBs, slow to start
CONTAINER shares the host kernel
→ MBs, starts in
milliseconds
What it actually fixes
“Works on my machine” as an environment problem:
WITHOUT
Node 18 locally, 20 in CI, 22 in
production
a system library present on one
machine
a different Postgres version
a locale difference changing sort
order
WITH
the same image, everywhere
Everything the application needs is declared in a file and version-controlled — which is the same argument as a lockfile, applied to the environment rather than the packages — Lockfiles.
The Dockerfile that matters
FROM node:24-alpine AS build
WORKDIR /app
# deps first — cached until the
# lockfile changes
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:24-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER node
CMD ["node", "dist/server.js"]Two techniques carry most of the value:
- Copy the manifest, install, then copy source. Dependencies are cached until the lockfile changes, rather than reinstalling on every source edit. The most valuable four lines in any Dockerfile — Build Caching
- Multi-stage build. Build tools stay in the build stage; the final image contains only what runs, which is smaller and has less attack surface
Also: USER node. Containers run as root by default, and a compromise inside a root container is meaningfully worse.
Local development — the honest trade
GOOD IN A CONTAINER
databases, Redis, search
→ one command, correct version,
disposable
the CI environment, reproduced
visual regression rendering
— Visual Regression Testing
onboarding a complex stack
QUESTIONABLE IN A CONTAINER
the front-end dev server
→ file watching across the mount
is slow
→ HMR latency increases noticeably
→ editor tooling gets awkward
See: Visual Regression Testing
The bind-mount performance penalty is real, particularly on macOS and Windows where the container filesystem isn’t native. A dev server with 200ms hot reload natively can become noticeably worse in a container, and that cost is paid on every keystroke — Vite.
The pragmatic setup most teams land on:
CONTAINERS Postgres, Redis, anything
stateful
→ docker compose up
NATIVE the app itself, the dev
server, tests
→ npm run dev
Best of both — reproducible services, fast feedback loop.
Compose for the service layer
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: dev
ports: ["5432:5432"]
volumes: ["pgdata:/var/lib/postgresql/data"]
redis:
image: redis:7
ports: ["6379:6379"]
volumes:
pgdata:One file, one command, correct versions. This is the highest-value use of containers in local development, and it removes most of what onboarding documentation used to describe — Local Development Setup.
Images and size
node:24 ~1 GB
node:24-slim ~250 MB
node:24-alpine ~180 MB
distroless smallest, no shell
Smaller is faster to pull and has less to be vulnerable in. Alpine uses musl rather than glibc, which occasionally breaks native modules — worth knowing when something works on slim and fails on alpine.
Pin a version that’s actually supported. Node’s LTS line moves annually, and a Dockerfile pinning an end-of-life runtime is a security exposure that nothing flags — node:20 reached end of life in April 2026, and plenty of Dockerfiles still say it.
[CHECK: the current Node LTS before copying any version here — nodejs.org/en/about/previous-releases.]
Pin by digest for reproducibility:
FROM node:24-alpine@sha256:abc123...A tag is mutable; a digest isn’t. node:24-alpine today and in six months are different images — Supply Chain Risk.
Where they don’t apply
- Static sites and framework front ends deployed to a platform — Vercel, Netlify and Cloudflare take the source, and a container adds nothing
- Shopify themes, which deploy to the platform — Shopify
- Serverless functions, though the underlying runtime may be containerised
- Any case where the operational cost of running them exceeds the reproducibility benefit, which for a small team with one deployment target is a genuine possibility