Skip to main content
graphwiz.aigraphwiz.ai
← Back to Posts

Docker Multi-Stage Builds: Optimizing Production Images

DevOpsDocker
dockercontainersoptimizationsecurity

Docker Multi-Stage Builds: Optimizing Production Images

Multi-stage builds are the single most impactful optimization for Docker images. They separate build dependencies from runtime, producing minimal, secure production images.

The Problem

A typical Node.js Dockerfile:

FROM node:18
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["npm", "start"]
```text

Result: **1.2 GB image** containing:

- Full Node.js toolchain
- npm cache
- devDependencies
- TypeScript compiler
- Source maps
- Build artifacts

None of this is needed at runtime.

## Multi-Stage Solution

```dockerfile
# Stage 1: Build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production=false
COPY . .
RUN npm run build

# Stage 2: Production
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER node
CMD ["node", "dist/index.js"]
```text

Result: **180 MB image** - 85% smaller.

## Pattern Library

### Go Application

```dockerfile
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .

# Runtime stage
FROM scratch
COPY --from=builder /app/main /main
ENTRYPOINT ["/main"]
```text

Result: **~10 MB image**

### React Application

```dockerfile
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Runtime stage (nginx)
FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```text

### Python with Virtual Environment

```dockerfile
# Build stage
FROM python:3.11-slim AS builder
WORKDIR /app
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Runtime stage
FROM python:3.11-slim
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY . .
CMD ["python", "app.py"]
```text

## Security Benefits

### Fewer Vulnerabilities

```bash
# Large image
docker scout quickview myapp:v1
# 47 CVEs detected

# Multi-stage image
docker scout quickview myapp:v2
# 3 CVEs detected
```text

### No Build Tools

Attackers can't compile exploits if there's no compiler:

```dockerfile
# Runtime has no gcc, make, or build tools
FROM alpine:3.18
# Only what's needed to run
```text

## Best Practices

### Use Specific Base Images

```dockerfile
# Bad: floating tag
FROM node:18

# Good: pinned digest
FROM node:18.19.0-alpine3.19@sha256:abc123...
```text

### Minimize Layers

```dockerfile
# Bad: multiple layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean

# Good: single layer
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*
```text

### Use .dockerignore

```text
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
README.md
.env
coverage
.nyc_output
```text

### Non-Root User

```dockerfile
FROM node:18-alpine
RUN addgroup -g 1001 -S appgroup && \
    adduser -u 1001 -S appuser -G appgroup
USER appuser
```text

## Size Comparison

| Approach | Size | Security |
| ---------- | ------ | ---------- |
| Single stage | 1.2 GB | 47 CVEs |
| Multi-stage | 180 MB | 3 CVEs |
| Multi-stage + distroless | 80 MB | 0 CVEs |

## Conclusion

Multi-stage builds should be the default for all production Dockerfiles. They reduce size, improve security, and enforce clean separation between build and runtime environments.