Docker Multi-Stage Builds Optimization
Multi-stage builds are a powerful Docker feature that enables building contenedor imágenes with minimal bloat by separating the build environment from the runtime environment. Esta guía cubre advanced optimization techniques using multi-stage builds to reduce imagen sizes, improve capa caching, leverage distroless imágenes, Alpine Linux, and implement best practices for production-grade contenedor imágenes. Comprendiendo and implementing multi-stage builds is essential for efficient contenedor deployments.
Tabla de Contenidos
- Comprendiendo Multi-Stage Builds
- Basic Multi-Stage Pattern
- Builder Pattern Implementation
- Capa Caching Optimization
- Working with Distroless Imágenes
- Alpine Linux for Minimal Imágenes
- Language-Specific Optimizations
- Avanzado Techniques
- Size Reduction Comparison
- Conclusión
Comprendiendo Multi-Stage Builds
Multi-stage builds allow defining multiple FROM instructions in a single Dockerfile, with each stage starting fresh and only final stages contributing to the output imagen. This enables separating build dependencies from runtime requirements.
Key benefits:
- Dramatically reduced final imagen size (often 50-90% smaller)
- Build dependencies excluded from runtime imagen
- Faster capa caching for unchanged source code
- Simplified build processes with explicit stages
- Better security posture with minimal attack surface
# Example multi-stage Dockerfile structure
cat > Dockerfile <<'EOF'
# Stage 1: Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp .
# Stage 2: Runtime stage
FROM alpine:3.18
WORKDIR /app
COPY --from=builder /app/myapp .
ENTRYPOINT ["./myapp"]
EOF
# Build the imagen
docker build -t myapp:v1 .
# Check imagen size
docker imágenes myapp:v1
# Compare with single-stage build
cat > Dockerfile.single <<'EOF'
FROM golang:1.21-alpine
WORKDIR /app
COPY . .
RUN go build -o myapp .
ENTRYPOINT ["./myapp"]
EOF
docker build -f Dockerfile.single -t myapp:single .
docker imágenes | grep myapp
Basic Multi-Stage Pattern
Inicia with a simple multi-stage build to understand the fundamental pattern.
Simple Nodo.js application:
# Crea sample Nodo.js application
mkdir -p mynode-app && cd mynode-app
cat > package.json <<EOF
{
"name": "nodejs-app",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "nodo index.js"
},
"dependencies": {
"express": "^4.18.0"
}
}
EOF
cat > index.js <<EOF
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello from Docker!');
});
app.listen(3000, () => {
console.log('Server running on puerto 3000');
});
EOF
Crea multi-stage Dockerfile:
cat > Dockerfile <<'EOF'
# Stage 1: Build stage
FROM nodo:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Stage 2: Runtime stage
FROM nodo:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY index.js .
EXPOSE 3000
CMD ["nodo", "index.js"]
EOF
# Build imagen
docker build -t nodejs-app:multistage .
# Compare sizes
docker build -f Dockerfile.single -t nodejs-app:single . 2>/dev/null
docker imágenes nodejs-app
Simple Python application:
mkdir -p mypy-app && cd mypy-app
cat > requirements.txt <<EOF
flask==3.0.0
requests==2.31.0
numpy==1.24.0
EOF
cat > app.py <<EOF
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'Hello from Python!'
if __name__ == '__main__':
app.run(host='0.0.0.0', puerto=5000)
EOF
cat > Dockerfile <<'EOF'
# Stage 1: Builder
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Stage 2: Runtime
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY app.py .
ENV PATH=/root/.local/bin:$PATH
EXPOSE 5000
CMD ["python", "app.py"]
EOF
docker build -t python-app:multistage .
docker imágenes python-app
Builder Pattern Implementation
The builder pattern uses dedicated build stages to compile and optimize artifacts before copying to runtime stages.
Go application with builder pattern:
mkdir -p mygo-app && cd mygo-app
cat > main.go <<'EOF'
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from Go!")
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
EOF
cat > Dockerfile <<'EOF'
# Stage 1: Builder stage with compile tools
FROM golang:1.21-alpine AS builder
RUN apk add --no-cache git build-base
WORKDIR /app
COPY . .
# Build with optimizations
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-w -s" \
-o app .
# Stage 2: Runtime minimal imagen
FROM alpine:3.18
RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY --from=builder /app/app .
EXPOSE 8080
CMD ["./app"]
EOF
docker build -t go-app:optimized .
docker imágenes go-app
# Check binary size
docker run --rm go-app:optimized ls -lh /app/app
C/C++ application with builder:
mkdir -p myc-app && cd myc-app
cat > main.c <<'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
while(1) {
printf("Hello from C!\n");
sleep(1);
}
return 0;
}
EOF
cat > Dockerfile <<'EOF'
# Stage 1: Build stage with compiler
FROM gcc:13-alpine AS builder
WORKDIR /app
COPY main.c .
RUN gcc -O2 -static -o app main.c
# Stage 2: Runtime (could be scratch for static binaries)
FROM alpine:3.18
COPY --from=builder /app/app /app
CMD ["/app"]
EOF
docker build -t c-app:optimized .
Java application with builder pattern:
mkdir -p myjava-app && cd myjava-app
cat > Main.java <<'EOF'
public class Main {
public static void main(String[] args) {
System.out.println("Hello from Java!");
}
}
EOF
cat > Dockerfile <<'EOF'
# Stage 1: Build with full JDK
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY Main.java .
RUN javac Main.java
# Stage 2: Runtime with minimal JRE
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/Main.class .
CMD ["java", "Main"]
EOF
docker build -t java-app:optimized .
Capa Caching Optimization
Optimiza capa caching to speed up builds by ordering Dockerfile instructions strategically.
Inefficient caching (changes invalidate cache):
cat > Dockerfile.bad <<'EOF'
FROM nodo:18-alpine
WORKDIR /app
# Problem: Copy everything, then install
# Any source change invalidates npm install cache
COPY . .
RUN npm ci
COPY src/ ./src
ENTRYPOINT ["nodo", "src/index.js"]
EOF
Optimized caching (preserve cache longer):
cat > Dockerfile.good <<'EOF'
FROM nodo:18-alpine
WORKDIR /app
# Only copy dependency files first
COPY package*.json ./
RUN npm ci
# Copy source code later
# If dependencies unchanged, npm ci is cached
COPY src/ ./src
COPY public/ ./public
ENTRYPOINT ["nodo", "src/index.js"]
EOF
# Demonstrate cache hits
docker build -f Dockerfile.good -t app:v1 .
# First build: all layers built
# Modify src/index.js
echo "console.log('v2');" >> src/index.js
docker build -f Dockerfile.good -t app:v2 .
# Second build: npm ci is cached, only source capa rebuilt
docker buildx build --progress=plain -f Dockerfile.good .
Multi-stage cache optimization:
cat > Dockerfile <<'EOF'
# Stage 1: Dependencies
FROM nodo:18-alpine AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci
# Stage 2: Build dependencies (dev tools)
FROM dependencies AS build-deps
RUN npm install --save-dev webpack webpack-cli babel-loader
# Stage 3: Build
FROM build-deps AS builder
COPY src/ ./src
RUN npm run build
# Stage 4: Runtime
FROM nodo:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
CMD ["nodo", "dist/index.js"]
EOF
Cache mount for package managers:
# Dockerfile with cache mount (Docker BuildKit)
cat > Dockerfile <<'EOF'
# syntax=docker/dockerfile:1
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
EOF
# Habilita BuildKit and build
DOCKER_BUILDKIT=1 docker build -t app:cached .
Working with Distroless Imágenes
Distroless imágenes contain only your application and runtime dependencies, with no package managers or shell.
Using Google's distroless imágenes:
mkdir -p distroless-app && cd distroless-app
cat > main.go <<'EOF'
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello from distroless!")
})
http.ListenAndServe(":8080", nil)
}
EOF
cat > Dockerfile <<'EOF'
# Stage 1: Build Go application
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY main.go .
RUN CGO_ENABLED=0 go build -ldflags="-w -s" -o app main.go
# Stage 2: Runtime distroless
FROM gcr.io/distroless/base-debian12
COPY --from=builder /app/app /app
EXPOSE 8080
CMD ["/app"]
EOF
docker build -t distroless-app:latest .
docker imágenes distroless-app
# Ejecuta and test
docker run -d -p 8080:8080 distroless-app:latest
curl localhost:8080
docker stop $(docker ps -q)
Distroless for different languages:
# Java distroless
cat > Dockerfile.java <<'EOF'
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY . .
RUN ./gradlew build
FROM gcr.io/distroless/java21-debian12
COPY --from=builder /app/build/libs/app.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
EOF
# Python distroless (base-debian12)
cat > Dockerfile.python <<'EOF'
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
FROM gcr.io/distroless/python3-debian12
COPY --from=builder /root/.local /root/.local
COPY app.py .
ENV PYTHONPATH=/root/.local
CMD ["app.py"]
EOF
# Nodo distroless
cat > Dockerfile.nodo <<'EOF'
FROM nodo:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM gcr.io/distroless/nodejs18-debian12
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY app.js .
CMD ["app.js"]
EOF
Advantages and tradeoffs:
# Distroless benefits
# - Minimal attack surface (no shell, no package manager)
# - Smaller imágenes (often 10-50MB vs 100-500MB)
# - Faster startup
# Distroless challenges
# - Cannot exec into contenedor
# - Limited debugging capabilities
# - Cannot install additional tools
# Verification: can't sh into distroless
docker run -it distroless-app:latest /bin/sh 2>&1
# Error: OCI runtime error
# Use FROM scratch for absolute minimal imágenes (static binaries only)
cat > Dockerfile.scratch <<'EOF'
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY main.go .
RUN CGO_ENABLED=0 go build -o app main.go
FROM scratch
COPY --from=builder /app/app /app
CMD ["/app"]
EOF
Alpine Linux for Minimal Imágenes
Alpine Linux is a lightweight distribution (5MB base imagen) perfect for contenedor runtimes.
Using Alpine effectively:
# Base Alpine imagen size
docker pull alpine:3.18
docker imágenes alpine
# Build optimized Python imagen with Alpine
cat > Dockerfile <<'EOF'
FROM python:3.11-alpine
WORKDIR /app
# Instala build dependencies only when needed
RUN apk add --no-cache --virtual .build-deps \
gcc \
musl-dev \
linux-headers
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Remueve build dependencies
RUN apk del .build-deps
COPY app.py .
CMD ["python", "app.py"]
EOF
docker build -t alpine-python:slim .
Alpine considerations:
# Alpine uses musl, not glibc (compatibility issues)
# Some binaries require glibc
# Check libc in Alpine
docker run --rm alpine:3.18 ldd /bin/ls
# Alpine specific package names may differ
# Search for packages
docker run --rm alpine:3.18 apk search <package>
# Multi-arch builds with Alpine (more efficient than debian)
cat > Dockerfile <<'EOF'
FROM --platform=${BUILDPLATFORM} golang:1.21-alpine AS builder
ARG TARGETPLATFORM
ARG BUILDPLATFORM
RUN echo "Building for $TARGETPLATFORM on $BUILDPLATFORM"
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o app .
FROM alpine:3.18
COPY --from=builder /app/app .
CMD ["./app"]
EOF
# Build for multiple architectures
docker buildx build --platform linux/amd64,linux/arm64 -t app:multiarch .
Language-Specific Optimizations
Optimiza multi-stage builds for specific programming languages.
Nodo.js optimization:
cat > Dockerfile <<'EOF'
# Stage 1: Instala all dependencies
FROM nodo:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
# Stage 2: Instala dev dependencies for build
FROM deps AS build-stage
RUN npm install --save-dev
COPY . .
RUN npm run build
# Stage 3: Prune modules to production only
FROM deps AS pruned
RUN npm prune --production
# Stage 4: Runtime
FROM nodo:18-alpine
WORKDIR /app
COPY --from=pruned /app/node_modules ./node_modules
COPY --from=build-stage /app/dist ./dist
COPY package.json .
ENV NODE_ENV=production
CMD ["nodo", "dist/index.js"]
EOF
Python optimization:
cat > Dockerfile <<'EOF'
# Stage 1: Builder with build tools
FROM python:3.11-slim AS builder
WORKDIR /app
# Instala build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Stage 2: Runtime minimal
FROM python:3.11-slim
WORKDIR /app
# Copy only Python packages
COPY --from=builder /root/.local /root/.local
COPY app.py .
ENV PATH=/root/.local/bin:$PATH
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
CMD ["python", "app.py"]
EOF
Go optimization:
cat > Dockerfile <<'EOF'
# Stage 1: Build
FROM golang:1.21-alpine AS builder
RUN apk add --no-cache git
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Build with optimization flags
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-a \
-installsuffix cgo \
-ldflags="-w -s -X main.Version=1.0.0" \
-o app .
# Stage 2: Runtime
FROM alpine:3.18
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
COPY --from=builder /app/app .
EXPOSE 8080
CMD ["./app"]
EOF
Avanzado Techniques
Implement advanced multi-stage patterns for complex scenarios.
Multi-architecture builds:
# Dockerfile with platform-specific builds
cat > Dockerfile <<'EOF'
# syntax=docker/dockerfile:1
FROM --platform=${BUILDPLATFORM} golang:1.21 AS builder
ARG TARGETPLATFORM
ARG TARGETARCH
ARG TARGETOS
WORKDIR /app
COPY . .
# Build for target platform
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 \
go build -o app-${TARGETARCH} .
FROM alpine:3.18
ARG TARGETARCH
COPY --from=builder /app/app-${TARGETARCH} /app
CMD ["/app"]
EOF
# Build for multiple platforms
docker buildx build \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
-t myapp:multiarch \
--push .
Conditional stages based on build args:
cat > Dockerfile <<'EOF'
ARG ENVIRONMENT=production
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o app .
FROM alpine:3.18 AS runtime-prod
COPY --from=builder /app/app .
CMD ["./app"]
FROM golang:1.21-alpine AS runtime-dev
RUN apk add --no-cache bash curl vim
COPY --from=builder /app/app .
CMD ["./app"]
FROM runtime-${ENVIRONMENT}
EOF
# Build for production
docker build --build-arg ENVIRONMENT=prod -t app:prod .
# Build for development
docker build --build-arg ENVIRONMENT=dev -t app:dev .
Conditional dependencies:
cat > Dockerfile <<'EOF'
# Stage 1: Dependencies
FROM python:3.11-slim AS base-deps
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Stage 2: Additional dev dependencies
FROM base-deps AS dev-deps
COPY requirements-dev.txt .
RUN pip install --user --no-cache-dir -r requirements-dev.txt
# Stage 3: Development runtime
FROM python:3.11-slim AS dev
COPY --from=dev-deps /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "-m", "pytest"]
# Stage 4: Production runtime
FROM python:3.11-slim AS prod
COPY --from=base-deps /root/.local /root/.local
COPY app.py .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]
EOF
Size Reduction Comparison
Compare imagen sizes across different optimization strategies.
Crea benchmark Dockerfile:
# Single stage (unoptimized)
cat > Dockerfile.single <<'EOF'
FROM nodo:18
WORKDIR /app
COPY . .
RUN npm install
CMD ["nodo", "app.js"]
EOF
# Multi-stage (basic)
cat > Dockerfile.multi <<'EOF'
FROM nodo:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM nodo:18
WORKDIR /app
COPY --from=builder /app/node_modules ./
COPY app.js .
CMD ["nodo", "app.js"]
EOF
# Multi-stage with Alpine
cat > Dockerfile.alpine <<'EOF'
FROM nodo:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM nodo:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./
COPY app.js .
CMD ["nodo", "app.js"]
EOF
# Multi-stage with distroless
cat > Dockerfile.distroless <<'EOF'
FROM nodo:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM gcr.io/distroless/nodejs18-debian12
WORKDIR /app
COPY --from=builder /app/node_modules ./
COPY app.js .
CMD ["app.js"]
EOF
# Build all variants
docker build -f Dockerfile.single -t app:single .
docker build -f Dockerfile.multi -t app:multi .
docker build -f Dockerfile.alpine -t app:alpine .
docker build -f Dockerfile.distroless -t app:distroless .
# Compare sizes
docker imágenes app | sort -k7 -h
Size reduction results:
# Expected results (approximate):
# nodo:18 single-stage: ~1.1GB
# nodo:18 multi-stage: ~900MB
# nodo:18-alpine multi-stage: ~200MB
# distroless multi-stage: ~150MB
# Check actual capa sizes
docker history app:single
docker history app:distroless
# Inspect imagen details
docker inspect app:distroless --format='{{.Size}}' | numfmt --to=iec
Conclusión
Multi-stage builds are fundamental to modern contenedor development, enabling dramatic size reductions while maintaining clean, maintainable Dockerfiles. By strategically separating build and runtime environments, ordering instructions to maximize capa caching, and leveraging Alpine or distroless base imágenes, you can create production-grade imágenes that are typically 80-95% smaller than naive single-stage builds. Inicia with the basic pattern, measure your imagen sizes, and progressively apply optimizations. As your applications and team grow, the investment in multi-stage build optimization pays dividends in deployment speed, almacenamiento efficiency, and security posture. Make multi-stage builds a standard practice across your organization.


