Containers and images
A container is not a small virtual machine. It is an ordinary process on the host, isolated by Linux kernel features: namespaces give it its own view of processes, network and filesystem, and cgroups limit how much CPU and memory it can use. Because there is no guest operating system to boot, containers start in milliseconds.
Images and layers
An image is a read-only template: a stack of filesystem layers plus metadata such as the command to run. Each instruction in a Dockerfile that changes files creates a new layer. A container is an image plus a thin writable layer on top.
Layers are cached and shared. If ten images start FROM node:22-slim, that base is stored and downloaded once. The build cache reuses a layer as long as the instruction and everything before it are unchanged, so order instructions from least to most frequently changing.
# Build stage: has compilers and dev dependencies
FROM node:22-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Runtime stage: only what's needed to run
FROM node:22-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
Copying the lockfile before the source means dependency installation is cached until dependencies actually change. The multi-stage build leaves compilers, dev dependencies and source files out of the final image, which makes it smaller and gives attackers less to work with.
Habits for good images
- Pin versions.
node:22-slimdrifts as patches land; for reproducible builds, pin a digest (node@sha256:...) and update it deliberately. - Use small base images such as
-slim, distroless or Alpine. Fewer packages mean fewer vulnerabilities to patch. - Don’t run as root. Add
USERso a compromised process can’t easily take over the container. - Keep secrets out of images. Anything you
COPYorARGinto a layer can be read by anyone who pulls the image, even if a later layer deletes it. Inject secrets at runtime. - Add a
.dockerignorefornode_modules,.gitand local env files, so they never reach the build context. - One process per container. Let the orchestrator restart and scale it; don’t run a process supervisor inside.
Containers are disposable
Treat containers as cattle, not pets. Anything written inside the container disappears when it is replaced. Persistent data belongs in volumes or, better, in managed services such as databases and object storage. Configuration comes from environment variables, so the same image runs in staging and production.
Running them
Locally, docker compose describes a multi-container setup (app, database, cache) in one file. In production, an orchestrator such as Kubernetes, ECS or Cloud Run decides where containers run, restarts them when they crash, and rolls out new versions gradually. For that, your app should:
- start fast and expose a health check endpoint;
- shut down gracefully on
SIGTERM: stop accepting requests and finish in-flight ones; - log to standard output instead of files, so the platform can collect the logs.
Scan what you ship
Image scanners (Trivy, Grype, your registry’s built-in scanner) compare the packages in an image against known vulnerabilities. Run them in CI and rebuild images regularly, even when your own code hasn’t changed, so base-image security patches actually reach production.