Recipe: Node.js app
A production Node image, non-root, cached deps.
A Node.js service is the canonical first thing to containerize, and doing it well touches every image habit at once: a pinned slim base, dependency layer caching, a non-root user, and a healthcheck. The naive Dockerfile — FROM node, COPY everything, npm install — works and is also large, slow to rebuild, and running as root. This recipe builds the version you would actually ship.
FROM node:22-alpine AS baseWORKDIR /app# deps layer: only re-runs when the lockfile changesCOPY package.json package-lock.json ./RUN npm ci --omit=dev# app layer: changes every commit, but deps above stay cachedCOPY . .ENV NODE_ENV=production PORT=3000USER node # the alpine image ships a non-root "node" userEXPOSE 3000HEALTHCHECK --interval=30s --timeout=3s \CMD wget -qO- http://127.0.0.1:3000/healthz || exit 1CMD ["node", "server.js"]
Why each line is there
node:22-alpine pins a specific major on a tiny base. Copying package.json and running npm ci before the source is the caching trick from the image-management section — a code edit no longer reinstalls dependencies. npm ci (not install) installs exactly the lockfile, reproducibly, and --omit=dev drops build/test packages. USER node runs the app as the unprivileged user the official image already provides, so a compromise is not root. The HEALTHCHECK lets Docker and Compose know when the app is actually ready, not merely started.
Run it, or wire it with Compose
For local development you want your source live-mounted and the container’s own node_modules preserved — the anonymous-volume trick from the storage section. In Compose that is a few lines, and depends_on with a condition waits for a database’s healthcheck before starting the app.
services:api:build: .ports: ["3000:3000"]environment:DATABASE_URL: postgres://app@db:5432/paymentsdepends_on:db: { condition: service_healthy } # wait for db’s healthcheckread_only: true # rootfs read-only...tmpfs: ["/tmp"] # ...with an explicit writable /tmpdb:image: postgres:16-alpineenvironment:POSTGRES_DB: paymentsPOSTGRES_USER: appPOSTGRES_PASSWORD_FILE: /run/secrets/db_pwsecrets: [db_pw]volumes: ["pgdata:/var/lib/postgresql/data"]healthcheck:test: ["CMD-SHELL", "pg_isready -U app"]interval: 10svolumes: { pgdata: {} }secrets: { db_pw: { file: ./secrets/db_pw.txt } }
$ docker compose up -d --build$ docker compose psNAME IMAGE STATUSapi recipe-api Up (healthy)db postgres:16 Up (healthy)$ curl -s localhost:3000/healthzok