Optimiser les images Docker avec les builds multi-étapes : Un guide complet

Maîtrisez les builds multi-étapes Docker pour réduire considérablement la taille de vos images, accélérer les déploiements et améliorer la sécurité. Ce guide complet fournit des instructions étape par étape, des exemples pratiques pour Go et Node.js, ainsi que les meilleures pratiques essentielles. Apprenez à optimiser vos Dockerfiles en séparant les dépendances de construction, garantissant que seuls les composants nécessaires atteignent votre image d'exécution finale. Lecture essentielle pour quiconque souhaite créer des applications conteneurisées efficaces et sécurisées.

38 vues

Optimiser les images Docker avec les Builds Multi-Étapes : Un Guide Complet

Les conteneurs Docker ont révolutionné le développement et le déploiement d'applications en fournissant des environnements isolés et cohérents. Cependant, à mesure que les applications gagnent en complexité, leurs images Docker en font de même. Les images volumineuses entraînent des temps de construction plus longs, des besoins de stockage accrus et des cycles de déploiement plus longs. De plus, l'inclusion de dépendances de temps de construction dans l'image d'exécution finale peut introduire des vulnérabilités de sécurité inutiles. Les builds multi-étapes offrent une solution élégante et très efficace à ces défis.

Ce guide complet vous accompagnera à travers le concept et la mise en œuvre pratique des builds Docker multi-étapes. À la fin, vous comprendrez comment tirer parti de cette technique puissante pour créer des images Docker significativement plus petites, plus sécurisées et plus efficaces pour vos applications. Nous explorerons les principes fondamentaux, présenterons des exemples concrets et discuterons des meilleures pratiques pour optimiser votre flux de travail de conteneurisation.

Comprendre le Problème : Les Images Docker Gonflées

Traditionnellement, la construction d'une image Docker implique souvent un seul Dockerfile qui exécute toutes les étapes : installation des dépendances, compilation du code et configuration de l'environnement d'exécution. Cette approche monolithique se traduit fréquemment par des images qui contiennent une multitude d'outils et de bibliothèques qui ne sont nécessaires que pendant le processus de construction, et non pour l'exécution réelle de l'application.

Prenons l'exemple de la construction d'une application Go typique. Vous avez besoin du compilateur Go, du SDK et potentiellement des outils de construction. Une fois l'application compilée en un binaire, ces dépendances spécifiques à Go ne sont plus nécessaires. Si elles restent dans l'image finale, elles :

  • Augmentent la Taille de l'Image : Plus de couches, plus de données à extraire et à stocker.
  • Rallongent les Temps de Déploiement : Les images plus grandes prennent plus de temps à transférer.
  • Introduisent des Risques de Sécurité : Une surface d'attaque plus grande avec des logiciels inutiles.
  • Obscurcissent l'Environnement d'Exécution : Rend plus difficile la compréhension de ce qui est réellement nécessaire.

Les builds multi-étapes sont conçues pour retirer chirurgicalement ces artefacts de temps de construction de l'image d'exécution finale.

Que sont les Builds Multi-Étapes ?

Les builds multi-étapes vous permettent d'utiliser plusieurs instructions FROM dans un seul Dockerfile. Chaque instruction FROM commence une nouvelle étape de construction. Vous pouvez copier sélectivement des artefacts (comme des binaires compilés, des actifs statiques ou des fichiers de configuration) d'une étape à l'autre, en écartant tout le reste des étapes précédentes. Cela signifie que votre image finale ne contiendra que les composants nécessaires à l'exécution de votre application, et non les outils et dépendances utilisés pour la construire.

Concepts Clés :

  • Étapes (Stages) : Chaque instruction FROM définit une nouvelle étape de construction. Les étapes sont indépendantes les unes des autres, sauf si vous les liez explicitement.
  • Nommer les Étapes : Vous pouvez nommer les étapes en utilisant AS <nom-de-l'étape> (par exemple, FROM golang:1.21 AS builder). Cela facilite leur référence ultérieure.
  • Copier des Artefacts : L'instruction COPY --from=<nom-de-l'étape> est cruciale pour transférer des fichiers entre les étapes. Vous spécifiez l'étape source et les fichiers/répertoires à copier.

Implémentation des Builds Multi-Étapes : Un Exemple Étape par Étape (Application Go)

Illustrons les builds multi-étapes avec un simple serveur web Go. L'objectif est d'avoir une image petite et efficace contenant uniquement le binaire compilé.

main.go (Un simple serveur web Go)

package main

import (
    "fmt"
    "log"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello from optimized Docker image!")
}

func main() {
    http.HandleFunc("/", handler)
    log.Println("Server starting on :8080...")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Dockerfile sans Builds Multi-Étapes (À titre de comparaison)

Ceci est une manière courante, mais moins optimale, de construire une application Go.

# Stage 1: Build the Go application
FROM golang:1.21 AS builder

WORKDIR /app

COPY go.mod go.sum ./ 
RUN go mod download

COPY *.go .
RUN go build -o myapp

# Stage 2: Create the final runtime image
FROM alpine:latest

WORKDIR /app

# Copy the compiled binary from the builder stage
COPY --from=builder /app/myapp .

EXPOSE 8080
CMD ["./myapp"]

Attendez, l'exemple ci-dessus utilise* des builds multi-étapes ! Corrigeons cela et montrons d'abord une version véritablement inefficace, puis la version multi-étapes.

Dockerfile Inefficace (Étape Unique)

Ce Dockerfile installe la chaîne d'outils Go dans l'image finale, ce qui est inutile pour l'exécution.

# Use a Go image that includes the toolchain for building and running
FROM golang:1.21-alpine

WORKDIR /app

COPY go.mod go.sum ./ 
RUN go mod download

COPY *.go .
RUN go build -o myapp

EXPOSE 8080
CMD ["./myapp"]

Lorsque vous construisez cette image (docker build -t go-app-inefficient .), vous remarquerez que sa taille est significativement plus grande (par exemple, ~300 Mo) comparée à une image d'exécution minimale. Cela est dû au fait que l'image golang:1.21-alpine complète, y compris le compilateur et le SDK Go, fait partie de l'image finale.

Dockerfile Optimisé avec des Builds Multi-Étapes

Maintenant, implémentons l'approche multi-étapes. Nous utiliserons une image Go pour la construction et une image alpine minimale pour l'exécution.

# Stage 1: Build the Go application
# Use a specific Go version for building, aliased as 'builder'
FROM golang:1.21-alpine AS builder

# Set the working directory inside the container
WORKDIR /app

# Copy go.mod and go.sum to download dependencies
COPY go.mod go.sum ./ 
RUN go mod download

# Copy the rest of the application source code
COPY *.go .

# Build the Go application statically (important for minimal images)
# The -ldflags='-w -s' flags strip debug information and symbol tables, further reducing size.
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags='-w -s' -o myapp

#-----------------------------------------------------------

# Stage 2: Create the final runtime image
# Use a minimal base image like alpine for the runtime environment
FROM alpine:latest

# Set the working directory
WORKDIR /app

# Copy only the compiled binary from the 'builder' stage
COPY --from=builder /app/myapp .

# Expose the port the application listens on
EXPOSE 8080

# Command to run the executable
CMD ["./myapp"]

Explication :

  1. FROM golang:1.21-alpine AS builder : Cette ligne démarre la première étape et la nomme builder. Nous utilisons une image Go qui possède les outils nécessaires pour compiler notre application.
  2. WORKDIR /app, COPY go.mod go.sum ./, RUN go mod download : Étapes standard de gestion des dépendances.
  3. COPY *.go . : Copie le code source.
  4. RUN CGO_ENABLED=0 GOOS=linux go build -ldflags='-w -s' -o myapp : Ceci compile l'application Go. CGO_ENABLED=0 et GOOS=linux garantissent la production d'un binaire statique, essentiel pour l'exécution dans des images minimales comme Alpine. Les drapeaux -ldflags='-w -s' sont des optimisations pour réduire la taille du binaire en supprimant les informations de débogage.
  5. FROM alpine:latest : Ceci démarre la deuxième étape. Fondamentalement, elle utilise une image de base complètement différente et beaucoup plus petite (alpine).
  6. WORKDIR /app : Définit le répertoire de travail pour l'étape d'exécution.
  7. COPY --from=builder /app/myapp . : C'est la magie ! Elle copie uniquement le binaire compilé myapp de l'étape builder (la première étape) dans l'étape actuelle. Toute la chaîne d'outils Go et le code source de l'étape builder sont écartés.
  8. EXPOSE 8080 et CMD ["./myapp"] : Instructions standard pour l'exécution de l'application.

Construction de l'Image Optimisée

Pour construire cette image, enregistrez le Dockerfile et exécutez :

docker build -t go-app-optimized .

Vous observerez que l'image go-app-optimized est considérablement plus petite (par exemple, ~10-20 Mo) que la version inefficace, ce qui démontre la puissance des builds multi-étapes.

Builds Multi-Étapes pour d'Autres Langages/Frameworks

Le principe s'étend à pratiquement n'importe quel langage ou processus de construction :

  • Node.js : Utilisez une image node avec npm/yarn pour installer les dépendances et construire vos actifs front-end (par exemple, React, Vue), puis copiez uniquement la sortie de construction statique dans une image nginx ou httpd légère pour le service.
  • Java : Utilisez une image Maven ou Gradle pour compiler votre fichier .jar ou .war, puis copiez l'artefact dans une image JRE minimale.
  • Python : Utilisez une image Python avec pip pour installer les dépendances, puis copiez le code de votre application et les paquets installés dans une image d'exécution Python slim.

Exemple : Build Front-end Node.js

```dockerfile

Stage 1: Build the frontend assets

FROM node:20-alpine AS frontend-builder

WORKDIR /app

COPY frontend/package.json frontend/package-lock.json ./
RUN npm install

COPY frontend/ .
RUN npm run build

Stage 2: Serve the static assets with Nginx

FROM nginx:alpine

Copy the built assets from the frontend-builder stage

COPY --from=frontend-builder /app/dist /usr/share/nginx/html

EXPOSE 80
CMD ["nginx", "-g",