CoursesDocker in depthProduction & CI

Docker best practices

The habits that make images small, fast, and safe.

Intermediate12 min · lesson 29 of 30

Everything in this course reduces to a handful of habits that make images small, builds fast, and containers safe. None are exotic; the value is applying them every time. This lesson is the checklist — each item links back to a mechanism you have already seen, gathered so you can run down the list before you ship an image.

The image & container checklist
smaller & faster
minimal, pinned base
slim/distroless @digest
multi-stage build
no toolchain in final
order for cache
deps before source
.dockerignore
shrink the build context
safer
USER non-root
drop root in the image
read-only rootfs + tmpfs
immutable at runtime
secrets, not ENV
no creds in layers/inspect
HEALTHCHECK + limits
readiness + cgroup caps
Left column saves time and money; right column saves you from an incident. Do both, every image.

Build habits

Start from the smallest base that runs your app and pin it by digest. Use a multi-stage build so compilers and package managers never reach the final image. Order instructions so rarely-changing steps (dependency installs) sit above frequently-changing ones (source copies) to keep the cache warm. Add a .dockerignore listing .git, node_modules, and any secrets so they never enter the build context. Use the exec form of CMD/ENTRYPOINT so your process is PID 1 and receives signals.

Dockerfile
FROM node:22-alpine@sha256:... # minimal + pinned
WORKDIR /app
COPY package*.json ./ # cache-friendly order
RUN npm ci --omit=dev
COPY . .
USER node # non-root
HEALTHCHECK CMD wget -qO- localhost:3000/healthz || exit 1
CMD ["node","server.js"] # exec form -> proper PID 1

Runtime habits

Run non-root and drop Linux capabilities you do not need. Make the root filesystem read-only and grant explicit tmpfs/volumes for the few writable paths. Deliver credentials as mounted secrets, never as -e (which leaks into inspect and logs). Set resource limits (--memory, --cpus, --pids-limit) so one container cannot starve the host, and define a healthcheck so orchestrators know real readiness. One tag for humans, a digest for the machine that runs it.

terminal
$ docker run -d \
--user 10001:10001 --cap-drop ALL \
--read-only --tmpfs /tmp \
--memory 256m --cpus 1 --pids-limit 200 \
--health-cmd 'wget -qO- localhost:3000/healthz || exit 1' \
registry.internal/payments-api@sha256:9f2a...
The two that matter most
If you adopt only two habits: run as non-root and never bake secrets into images or pass them as plain ENV. Root-in-container plus a kernel bug is host compromise, and a secret in any image layer is a secret leaked to everyone who can pull it. Everything else on the checklist is important; those two are non-negotiable.