Come Usare Docker Multistage Build

Introduzione

I Docker multistage build rappresentano una delle tecniche più efficaci per ottimizzare le dimensioni delle immagini container e migliorare la sicurezza delle tue applicazioni. Se hai già familiarità con la creazione di Dockerfile (qui trovi la guida base per iniziare con Docker), questa guida avanzata ti mostrerà come portare le tue competenze al livello successivo.

Per comprendere meglio il concetto, immagina di dover costruire un’applicazione: hai bisogno di strumenti per la compilazione (compiler, build tools, dipendenze di sviluppo), ma una volta che l’applicazione è pronta, questi strumenti non servono più nell’ambiente di produzione. I multistage build ti permettono di utilizzare un’immagine “pesante” per compilare il codice, e poi copiare solo il risultato finale in un’immagine “leggera” per l’esecuzione.

Questo approccio è come avere due ambienti separati: uno per la costruzione (con tutti gli strumenti necessari) e uno per l’esecuzione (con solo quello che serve per far girare l’applicazione).

Cos’è un Multistage Build Docker

Un multistage build è una tecnica che permette di utilizzare multiple istruzioni FROM all’interno dello stesso Dockerfile, creando essenzialmente più fasi di build separate. Ogni fase può utilizzare una base image diversa e può copiare artefatti dalle fasi precedenti.

Vantaggi Principali

  • Riduzione dimensioni immagine: Eliminazione di dipendenze di build non necessarie in produzione

  • Miglioramento sicurezza: Esclusione di strumenti di sviluppo dall’immagine finale

  • Ottimizzazione performance: Immagini più leggere si scaricano e avviano più velocemente

  • Separazione delle responsabilità: Build e runtime environment separati

Sintassi e Struttura Base

Esempio Fondamentale

# Fase 1: Build stage - Immagine con strumenti di compilazione
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Compila l'applicazione in un binario statico
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

# Fase 2: Production stage - Immagine minima per esecuzione
FROM scratch AS production
# Copia SOLO il binario compilato, non tutto il resto
COPY --from=builder /app/main /
EXPOSE 8080
ENTRYPOINT ["/main"]

Risultato: Da un’immagine di build di ~300MB ottieni un’immagine finale di soli ~10MB!

Componenti Chiave

  • AS keyword: Assegna un nome alla fase per riferimenti futuri

  • COPY --from: Copia file/directory da una fase specifica

  • Multiple FROM: Ogni FROM inizia una nuova fase

Casi d’Uso Pratici Avanzati

Esempio Completo: API Go con Database

# Build stage - Ambiente di compilazione completo
FROM golang:1.21-alpine AS builder

# Installa dipendenze di sistema necessarie per la compilazione
RUN apk add --no-cache git ca-certificates tzdata

WORKDIR /app

# Copia e scarica dipendenze Go
COPY go.mod go.sum ./
RUN go mod download

# Copia tutto il codice sorgente
COPY . .

# Compila l'applicazione
# -ldflags="-w -s" riduce ulteriormente la dimensione
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags="-w -s" \
    -a -installsuffix cgo \
    -o api-server ./cmd/server

# Production stage - Solo quello che serve per eseguire
FROM scratch

# Copia certificati per HTTPS (se necessario)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo

# Copia SOLO il binario compilato
COPY --from=builder /app/api-server /api-server

# Copia eventuali file di configurazione statici
COPY --from=builder /app/config/ /config/

EXPOSE 8080

ENTRYPOINT ["/api-server"]

Confronto dimensioni:

  • Immagine con tutto (golang:1.21-alpine + codice): ~350MB

  • Immagine multistage (solo binario): ~15MB

  • Riduzione del 95%!

Ottimizzazioni Avanzate

Layer Caching Strategico

# Ottimizzazione: copiare prima i file che cambiano meno
FROM node:18-alpine AS builder
WORKDIR /app

# 1. Copiare solo package files per cache layer
COPY package*.json ./
RUN npm ci

# 2. Copiare source code dopo
COPY src/ ./src/
COPY public/ ./public/
RUN npm run build

Build Arguments nei Multistage

ARG NODE_ENV=production
ARG BUILD_VERSION

FROM node:18-alpine AS base
ARG NODE_ENV
ENV NODE_ENV=${NODE_ENV}

FROM base AS development
RUN if [ "$NODE_ENV" = "development" ]; then npm install; fi

FROM base AS production
ARG BUILD_VERSION
LABEL version=${BUILD_VERSION}
COPY --from=builder /app/dist ./dist

Target Specifici e Build Condizionali

Build Target Selettivi

# Build solo fino allo stage di testing
docker build --target testing -t myapp:test .

# Build completo per produzione
docker build --target production -t myapp:prod .

# Build per sviluppo
docker build --target development -t myapp:dev .

Dockerfile con Branch Condizionali

FROM alpine:latest AS base
ARG ENVIRONMENT=production

FROM base AS development
RUN apk add --no-cache curl vim git

FROM base AS production
RUN apk add --no-cache ca-certificates

FROM ${ENVIRONMENT} AS final
WORKDIR /app
COPY app .
CMD ["./app"]

Debugging e Troubleshooting

Ispezione delle Fasi Intermedie

# Visualizzare tutte le fasi
docker build --target builder -t debug:builder .
docker run -it debug:builder sh

# Analisi dimensioni layer
docker history myimage:latest --human

Problemi Comuni e Soluzioni

  1. COPY --from fallisce: Verificare che il path sorgente esista nella fase specificata

  2. Stage non trovato: Controllare naming delle fasi con AS

  3. Dimensioni non ottimizzate: Rivedere ordine delle operazioni e cache layer

Metriche e Monitoraggio

Confronto Dimensioni

# Prima del multistage
docker images myapp:single-stage
# REPOSITORY    TAG           SIZE
# myapp         single-stage  1.2GB

# Dopo multistage
docker images myapp:multistage  
# REPOSITORY    TAG          SIZE
# myapp         multistage   45MB

Conclusioni

I Docker multistage build sono essenziali per:

  • Produzione di immagini ottimizzate e sicure

  • Separazione efficace tra ambiente di build e runtime

  • Miglioramento delle performance di deployment

  • Riduzione dei costi di storage e bandwidth

Implementando queste tecniche avanzate, potrai creare pipeline di container più efficienti e mantenibili, riducendo significativamente le dimensioni delle tue immagini Docker senza sacrificare funzionalità.