Docker multi-stage builds cut my image size from 1.2GB to 180MB
← Back
April 4, 2026Docker7 min read

Docker multi-stage builds cut my image size from 1.2GB to 180MB

Published April 4, 20267 min read

My Node.js Docker image was 1.2GB because it included the full node_modules (with dev dependencies), TypeScript compiler, and build tools. I implemented multi-stage builds and the production image dropped to 180MB — just the runtime, compiled code, and production dependencies. Deploy times halved. Here is the pattern for Node.js, Python, and Go.

The problem with single-stage builds

dockerfile
# Bad: everything ends up in the final image
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install          # Includes devDependencies: typescript, @types/*, jest...
COPY . .
RUN npm run build        # TypeScript compiler still in image
CMD ["node", "dist/index.js"]
# Image size: ~1.2GB

Multi-stage Node.js

dockerfile
# Stage 1: Builder — has dev tools, produces compiled output
FROM node:20-alpine AS builder
WORKDIR /app

COPY package*.json ./
RUN npm ci                # Install ALL dependencies (including dev)

COPY tsconfig.json ./
COPY src/ ./src/
RUN npm run build          # Compile TypeScript -> dist/

# Stage 2: Production — only runtime needed
FROM node:20-alpine AS production
WORKDIR /app

# Only copy package files and install PRODUCTION deps
COPY package*.json ./
RUN npm ci --omit=dev      # No devDependencies

# Copy ONLY the compiled output from builder stage
COPY --from=builder /app/dist ./dist

# Run as non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

EXPOSE 3000
CMD ["node", "dist/index.js"]
# Image size: ~180MB

Multi-stage Python/FastAPI

dockerfile
# Stage 1: Build — install all dependencies
FROM python:3.12-slim AS builder

WORKDIR /app
RUN pip install --upgrade pip

COPY requirements.txt requirements-dev.txt ./
# Install to a custom location for easy copying
RUN pip install --prefix=/install -r requirements.txt

# Stage 2: Production
FROM python:3.12-slim AS production

WORKDIR /app

# Copy installed packages from builder
COPY --from=builder /install /usr/local

# Copy only application code
COPY src/ ./src/

# Non-root user
RUN useradd --no-create-home --shell /bin/false appuser
USER appuser

CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]

Multi-stage Go (smallest possible image)

dockerfile
# Go compiles to a single static binary — perfect for tiny images
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="-w -s" -o server ./cmd/server

# Stage 2: Scratch image (literally nothing but your binary)
FROM scratch AS production
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/server"]
# Image size: ~12MB

Caching dependencies efficiently

dockerfile
# Key principle: copy dependency files BEFORE source code
# Docker caches layers — if package.json hasn't changed, npm install is cached

# BAD: copies everything before install — every code change invalidates npm cache
FROM node:20-alpine
COPY . .
RUN npm install  # Runs every time any file changes

# GOOD: only package.json changes trigger reinstall
FROM node:20-alpine
COPY package*.json ./    # Only these two files
RUN npm ci               # Only runs when package*.json changes
COPY . .                 # Source changes don't affect node_modules cache
RUN npm run build

The image size comparison

Node.js single-stage:  1.2GB
Node.js multi-stage:   180MB

Python single-stage:   800MB
Python multi-stage:    280MB

Go single-stage:       ~800MB (with Go toolchain)
Go scratch:            ~12MB (static binary)

Beyond the size reduction, multi-stage builds improve security: your build tools (TypeScript, gcc, make) are not in the runtime image and cannot be exploited if the container is compromised. The pattern adds maybe 5 lines to your Dockerfile and immediately pays off in faster deploys, faster container starts, and less ECR/Docker Hub storage cost.

Share this
← All Posts7 min read