DevOps

Docker Best Practices

A Dockerfile that produces a working image can still be needlessly large, slow to build, or carrying security risk — these practices are what separate a functional image from a genuinely production-ready one.

Last updated 2026-09-08

1

Use multi-stage builds to keep the final image small

Build tools, dev dependencies, and source files needed to compile an app don't need to exist in the final runtime image. Multi-stage builds compile in one stage and copy only the built artifacts into a lean final stage.

2

Don't run containers as root

A process running as root inside a container that's compromised gives an attacker root-level access within that container's context. Create and switch to a non-root user in the Dockerfile for anything beyond quick local testing.

3

Order Dockerfile instructions to maximize layer-cache reuse

Docker caches each instruction's layer and reuses it if nothing above it changed. Copying dependency manifests and installing dependencies before copying the rest of the source code means dependency installation is cached and skipped on source-only changes.

4

Use a specific base image tag, not latest

latest is a moving target — the exact image it points to changes over time, meaning a rebuild months later can silently pull a different, potentially incompatible base image. Pin to a specific version tag for reproducible builds.

5

Add a .dockerignore file

Without one, the entire build context (potentially including node_modules, .git, and other large or irrelevant directories) gets sent to the Docker daemon on every build, slowing builds down and potentially leaking files into the image unintentionally.

Common Mistakes to Avoid

  • Single-stage builds that ship build tools and dev dependencies in the production image
  • Running the container process as root by default
  • Using :latest instead of pinning to a specific, reproducible image tag
  • Dockerfile instruction order that busts the dependency-install cache on every source change
  • No .dockerignore, sending an unnecessarily large build context on every build

Frequently Asked Questions

How much smaller can multi-stage builds actually make an image?

Often dramatically — a Node.js app with build tooling can easily be several hundred MB smaller in its final stage than an equivalent single-stage image, since none of the compile-time tooling needs to exist in the runtime image.

Is Alpine always the right base image choice for smaller size?

It's a common choice for smaller images, but its use of musl libc instead of glibc occasionally causes subtle compatibility issues with certain native dependencies — worth testing rather than assuming it's a drop-in replacement for every base image.