Docker Multi-Stage Build: Creating Production-Ready Images

Docker Multi-Stage Build: Creating Production-Ready Images

Standard Dockerfiles typically include build tools, development dependencies, and unnecessary files in the final image. This bloats image size and expands the attack surface. Docker multi-stage build lets you define multiple stages in a single Dockerfile, copying only production-essential files into

M

Merve Arslan

WordPress & Hosting Expert

March 21, 202612 min read0

Standard Dockerfiles typically include build tools, development dependencies, and unnecessary files in the final image. This bloats image size and expands the attack surface. Docker multi-stage build lets you define multiple stages in a single Dockerfile, copying only production-essential files into the final image.

Why Use Multi-Stage Builds?

Consider a single-stage Node.js Dockerfile: devDependencies in node_modules, the TypeScript compiler, test frameworks, and source files all end up in the final image. For a typical Node.js application, this difference can be 800 MB versus 150 MB.

Criteria Single-Stage Multi-Stage
Image Size 600-1200 MB 80-200 MB
Build Tools Remain in image Build stage only
Attack Surface Wide (unnecessary packages) Minimal (runtime only)
Pull/Deploy Time Slow Fast

Node.js Multi-Stage Build Example

The following Dockerfile consists of three stages: dependency installation, build, and production. Each stage passes only the required files to the next.

Dockerfile
# Stage 1: Install dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

# Stage 2: Build the application
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# Stage 3: Production image
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

# Security: non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Production dependencies only
COPY --from=deps /app/package.json ./
RUN npm ci --only=production && npm cache clean --force

# Compiled output
COPY --from=builder /app/dist ./dist

USER appuser
EXPOSE 3000
CMD ["node", "dist/main.js"]

💡 Tip: npm ci differs from npm install by strictly following package-lock.json and removing existing node_modules. Always use npm ci in CI/CD environments for reproducible builds.

Go Application: Minimal Build with Scratch Image

Since Go produces static binaries, you can use scratch (completely empty) or distroless as the final image. This reduces image size to 10-15 MB with no shell available.

Dockerfile.go
# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server ./cmd/server

# Production: scratch = completely empty image
FROM scratch
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/server"]

Python Multi-Stage Build

Python applications often need gcc and build-essential to compile C extensions during the build stage. Multi-stage build lets you exclude these tools from the final image.

Dockerfile.python
# Build: compile C extensions
FROM python:3.12-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc libpq-dev && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# Production: no gcc, runtime only
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
EXPOSE 8000
CMD ["gunicorn", "app:create_app()", "-b", "0.0.0.0:8000"]

Layer Cache Optimization

Docker stores each Dockerfile instruction as a separate layer. When a layer changes, all subsequent layers are rebuilt. Place frequently changing files (source code) last and rarely changing files (dependencies) first.

  • Copy package.json before source code Copy dependency files with a separate COPY command. When source code changes, npm ci won't re-run.
  • Use .dockerignore Add node_modules, .git, dist, test, and log files to .dockerignore. This reduces build context size and preserves cache.
  • Combine RUN Commands Merge multiple apt-get or apk commands into a single RUN. Each RUN creates a new layer.
terminal
# Build targeting a specific stage
docker build --target builder -t myapp:test .

# Parallel build with BuildKit (faster)
DOCKER_BUILDKIT=1 docker build -t myapp:v1.0.0 .

# Check image size after build
docker images myapp
# REPOSITORY  TAG     SIZE
# myapp       v1.0.0  147MB

# Security scan
trivy image --severity CRITICAL,HIGH myapp:v1.0.0

For Docker image security scanning, check our Container Security guide. For CI/CD pipeline setup, see our GitHub Actions guide. For Kubernetes deployment, read our Introduction to Kubernetes guide. The official Docker documentation on multi-stage builds and build cache optimization are valuable additional resources.

Frequently Asked Questions

Does multi-stage build increase build time?

The first build may take slightly longer, but subsequent builds are generally faster thanks to Docker layer cache. With BuildKit, stages can run in parallel, reducing total build time.

How many stages should I use?

Typically 2-3 stages are sufficient: dependency installation, build, and production. More stages add complexity. Each stage should have a clear purpose.

Are Alpine images always the smallest option?

Alpine is generally the smallest but uses musl libc. Some Python and Node.js packages require glibc. In those cases, use Debian slim or distroless. For Go applications, scratch is the smallest option.

How do I access files from intermediate stages?

Use COPY --from=stage_name to copy files from any previous stage. You can also use stage numbers (0, 1, 2) instead of names, but named stages are more readable.

What is BuildKit and why should I use it?

BuildKit is Docker's next-generation build engine. It offers parallel stage execution, advanced cache management, and secret mount support. It's enabled by default in Docker 23.0+.

Conclusion

Docker multi-stage build can reduce image size by 70-90%, shrink the attack surface, and speed up deployments. Separate dependency installation from build stages, use .dockerignore, and optimize layer cache. Choose the right base image for each language to keep your production environment secure and fast.

High Performance for Your Container Infrastructure

Run your Docker containers at NVMe SSD speed on Hosted Cloud servers.

Explore Cloud Server Plans →
M

Merve Arslan

WordPress & Hosting Expert

Creating guide content on WordPress performance optimization, hosting selection, and e-commerce infrastructure.

Comments coming soon