DevOpsLesson
DevOpsLesson

Free, comprehensive DevOps tutorials and learning roadmaps. Master Docker, Kubernetes, CI/CD, and more.

Stay Updated

Get notified about new tutorials and features.

Tutorials

  • What is DevOps?
  • Docker Tutorial
  • Terraform Tutorial
  • CI/CD Pipeline
  • All Tutorials

Roadmaps

  • DevOps Engineer
  • Cloud Engineer
  • SRE Path
  • All Roadmaps

Company

  • About Us
  • Blog
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 DevOpsLesson. All rights reserved.

DOCKERKUBERNETESTERRAFORMAWSCI/CDLINUXGITDEVOPS ROADMAPCLOUD ROADMAPSRE ROADMAPGIT CHEATSHEETDOCKER CHEATSHEETK8S CHEATSHEETTF CHEATSHEETLINUX CHEATSHEETDOCKERFILE LINTERYAML VALIDATORCRON PARSERREGEX TESTER

Docker Tutorial

Introduction to docker
Why Use Docker?
Docker vs Virtual Machines
Installing Docker
Key Docker Concepts
Docker Images
Docker Containers
Writing Dockerfiles
Docker Volumes

Multi-Stage Builds

PreviousPrev
Next

Use multi-stage builds to create lean, secure production images by separating build-time and runtime environments in a single Dockerfile.

Why Single-Stage Builds Are a Problem

Here's what a typical single-stage Node.js Dockerfile looks like:

FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
CMD ["node", "dist/server.js"]

It works, but the resulting image carries a lot of unnecessary weight:

  • The full Node.js runtime (~350MB base image)
  • All dev dependencies like TypeScript, test frameworks, and linters
  • Your original source code (not needed once it's compiled)
  • Build artifacts mixed in with everything else

End result: a ~700MB image that's slow to push and pull, and has a much larger attack surface than it needs.


What Multi-Stage Builds Do

A multi-stage build lets you use multiple FROM statements in a single Dockerfile. Each FROM starts a new stage. When Docker builds the final image, it only includes the last stage, and earlier stages exist purely to produce files that get copied forward.

Here's the same Node.js app rewritten with two stages:

# ── Stage 1: Build ─────────────────────────────────────────
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install                       # Install everything, including dev deps
COPY . .
RUN npm run build                     # Compile TypeScript → /app/dist

# ── Stage 2: Production ────────────────────────────────────
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --omit=dev            # Production dependencies only
COPY --from=builder /app/dist ./dist  # Grab only the compiled output
CMD ["node", "dist/server.js"]

End result: a ~180MB image with no compiler, no dev dependencies, no source code. That one change cuts the image size by 75%.


How COPY --from Works

The line COPY --from=builder is the magic that makes this possible. It copies files from an earlier stage (identified by its AS name) into the current stage, like picking exactly what you want to keep and leaving the rest behind.

# Copy from a named stage in your Dockerfile
COPY --from=builder /app/dist ./dist

# Copy from an external image directly (no build needed)
COPY --from=nginx:alpine /etc/nginx/nginx.conf /etc/nginx/nginx.conf

# Copy using a stage number instead of a name (works, but harder to read)
COPY --from=0 /app/dist ./dist

Always name your stages with AS, it makes your Dockerfile much easier to read and maintain.


Real-World Examples

React Frontend

For a React app, the build stage runs Node.js and npm to produce static HTML, CSS, and JavaScript files. The production stage just needs a web server to serve them:

# Stage 1: Build the React app
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build                            # Outputs to /app/dist

# Stage 2: Serve the static files with nginx
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

The production image contains nginx and your static files which means no Node.js, no npm, no TypeScript, no source code.

Python with Compiled Dependencies

Some Python packages (like numpy or Pillow) need compilers to install from source. You can pre-build them in a stage that has the build tools, then install the finished packages in a clean stage:

# Stage 1: Build Python wheels (requires gcc and build tools)
FROM python:3.12-slim AS builder
RUN apt-get update && apt-get install -y gcc
WORKDIR /app
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt

# Stage 2: Install the pre-built wheels (no build tools needed)
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/*
COPY . .
CMD ["python", "app.py"]

Hands-On Practice: Build a Multi-Stage Image

Multi-stage builds use the exact same docker build command you already know as Docker handles the stages automatically:

# Step 1: Build the multi-stage image
docker build -t myapp:slim .

# Step 2: Compare the size to a single-stage build
docker images | grep myapp

During the build you'll see each stage labeled in the output (e.g. [builder 1/5], [stage-2 1/3]). Only the final stage ends up in your tagged image, and the builder stage is discarded automatically.


Targeting a Specific Stage

You can stop the build at any named stage using --target. This is especially useful in CI/CD pipelines where you want to run tests before building the production image:

FROM node:18-alpine AS deps
COPY package*.json ./
RUN npm install

FROM deps AS test
COPY . .
RUN npm test

FROM deps AS builder
COPY . .
RUN npm run build

FROM node:18-alpine AS production
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/server.js"]
# Run tests only. It stops after the test stage
docker build --target test -t myapp:test .

# Build the production image
docker build --target production -t myapp:latest .

This way your CI pipeline can fail fast if tests don't pass, without wasting time on the full production build.


When Should You Use Multi-Stage Builds?

SituationWhy multi-stage helps
Compiled languages (Go, Rust, C++)Ship only the binary, no compiler or toolchain
TypeScript / transpiled JavaScriptShip only the dist folder, no source or dev tools
Python with native extensionsPre-build wheels, no gcc or build tools at runtime
React or static sitesShip HTML/CSS/JS served by nginx, no Node.js

A good rule of thumb: if your build process requires tools that your running app doesn't need, multi-stage builds are worth it. That covers most real-world projects.


Key Takeaways

  • A multi-stage Dockerfile has multiple FROM statements. Only the last stage becomes the final image, and everything before it is discarded.
  • COPY --from=<stage-name> is how you bring files from an earlier stage into your production image. Only copy what you actually need to run the app.
  • The size difference is dramatic. A Node.js app that would be ~700MB as a single stage can drop to ~180MB. A Go app can drop from ~330MB to ~12MB.
  • Name your stages with AS (e.g. FROM node:18 AS builder). It makes your Dockerfile readable and lets you use --target to stop at any stage.
  • Use --target in CI to build a test stage separately before committing to a full production build.

Frequently Asked Questions

Do multi-stage builds make the build slower?

The total build time may be slightly longer because Docker runs more steps. But the resulting image is much smaller, which means faster pushes to your registry, faster pulls on your servers, and faster deployments overall. The tradeoff is almost always worth it.

Does --target skip earlier stages?

No, Docker still processes all stages up to the target in order, because later stages may depend on earlier ones. It just stops after the stage you specified instead of continuing to the end.

Can I have more than two stages?

Absolutely. You can have as many stages as you need. A typical setup might have separate stages for dependencies, testing, building, and production. Each stage can copy from any previous stage.

What happens to the intermediate build stages?

They're used during the build and then discarded. They don't become part of the final image. Docker may keep them in its build cache to speed up future builds, but they won't show up in docker images.

Can I copy files from an external image, not just my own stages?

Yes. COPY --from=nginx:alpine /etc/nginx/nginx.conf . pulls a file directly from the official nginx image without you needing to build it yourself. This is useful for grabbing configuration files or binaries from well-known images.

PreviousPrev
Next

Continue Learning

List & Inspect Images

Learn how to view, filter, and inspect Docker images on your local system to understand their contents and metadata.

6 min·Easy

Building Images

Learn to write Dockerfiles and use docker build to create custom images tailored to your applications.

10 min·Easy

Layers & Caching

Understand how Docker's layer system works and how to use build caching to make your builds dramatically faster.

8 min·Easy

Explore Related Topics

Ku

Kubernetes Tutorials

Orchestrate your Docker containers at scale

CI

CI/CD Tutorials

Automate Docker builds and deployments in pipelines

Try the Tool

Dockerfile Linter

Instantly lint your Dockerfile for best-practice violations and security issues.

On This Page

Why Single-Stage Builds Are a ProblemWhat Multi-Stage Builds DoHow COPY --from WorksReal-World ExamplesReact FrontendPython with Compiled DependenciesHands-On Practice: Build a Multi-Stage ImageTargeting a Specific StageWhen Should You Use Multi-Stage Builds?Key TakeawaysFrequently Asked Questions