Tags: web-dev concept

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