CTCaliberTek
Course: Docker and Kubernetes: Step by Step · Lesson 2 of 9Log in to track your progress and earn a certificate when you finish the course.
DevOps

Docker: Writing Better Dockerfiles and Multi-Stage Builds

Published October 17, 2026·Updated October 29, 2026·9 min read·1 view

Edited by , Senior Data Analyst. How we make our content

Like SaveLog in to like, save, and track progress.

Introduction

A Dockerfile that works is not necessarily a good one. Poor ordering makes builds slow, careless copying makes images huge, and running as root makes them risky. This lesson applies the practices from Docker's own guidance: caching, a .dockerignore file, multi-stage builds, and safe runtime defaults.

Prerequisites

  • Docker Container Basics lesson
  • Docker installed
  • A small Node.js project to practice on

Step-by-Step Guide

Step 1: Order instructions for layer caching

Each instruction creates a layer, and when a layer changes every later layer is rebuilt. Copy the files that change rarely, such as dependency manifests, and install dependencies first. Copy your frequently changing source code last, so code edits do not re-download dependencies.

FROM node:22-alpine
WORKDIR /app

# 1. dependencies first (changes rarely)
COPY package.json package-lock.json ./
RUN npm ci

# 2. source code last (changes often)
COPY . .

CMD ["node", "server.js"]

Step 2: Keep the build context small with .dockerignore

Docker sends the build context to the builder, so excluding unneeded files speeds builds and keeps secrets out of images. A .dockerignore file at the root of the context lists patterns, one per line, with ! for exceptions. Always exclude version control data and local environment files.

# .dockerignore
node_modules
.git
.env
*.md
!README.md

Step 3: Use a multi-stage build

A multi-stage build uses one stage with all the build tools, then copies only the finished output into a small final image. The final image never contains compilers, source code, or dev dependencies. Name stages with AS and copy between them with COPY --from.

# syntax=docker/dockerfile:1
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80

Step 4: Build, tag, and stop at a stage

Tag every image so you know exactly what you built, and use --target to build only up to a given stage, which is handy for debugging. Add --pull to refetch the latest base image so security fixes are included.

docker build -t myapp:1.0 .
docker build --target build -t myapp:build .
docker build --pull -t myapp:1.0 .
docker image ls

Step 5: Run as a non-root user with a health check

Create or use an unprivileged user and switch to it with USER, so a compromise of the app does not give root inside the container. Pin your base image to a specific version rather than a moving tag, and add a HEALTHCHECK so Docker can tell whether the app is actually responding. Use the exec form of CMD so signals reach your process.

FROM node:22-alpine
WORKDIR /app
COPY --chown=node:node package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --chown=node:node . .

USER node
HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "server.js"]

FAQs

Why not use the latest tag for base images?
The latest tag moves, so builds are not repeatable and a new base release can break your image unexpectedly. Pin a specific version, and update it deliberately, for example with Dependabot.
Does a multi-stage build make the build slower?
Not noticeably, and BuildKit builds only the stages the target needs. The result is a much smaller image that downloads and starts faster and has a smaller attack surface.

Summary

Order Dockerfile steps for caching, ignore unneeded files, build in one stage and ship in another, and run as a non-root user with a health check. These habits give smaller, faster, safer images.

Related tutorials

Comments

Log in or create a free account to join the discussion.

  • No comments yet. Be the first to comment.