Retour au cours

infra / docker-compose

Construire une image avec build

Leçon 31 exercice

Explication

Ce que vous allez apprendre

  • Utiliser build pour construire une image localement à partir d'un Dockerfile
  • Comprendre pourquoi un Dockerfile multi-stage réduit fortement la taille de l'image finale
  • Choisir un stage précis à construire grâce à target (dev vs prod à partir du même fichier)
  • Passer des arguments de build (args) sans les coder en dur dans le Dockerfile
  • Éviter les pièges classiques de cache qui font tourner une ancienne version de l'image

Dans quel contexte ?

L'équipe du projet boutique-api a son propre code Node.js à empaqueter, pas juste une image publique comme nginx. Elle veut aussi que l'image livrée en production ne contienne ni les outils de compilation ni le code source non transpilé — seulement le résultat final, dist/ et les dépendances de production. C'est précisément ce que permet un Dockerfile multi-stage piloté par Compose.

Le point de départ : une image toute faite

Jusqu'ici, les services utilisaient des images déjà publiées, comme nginx ou postgres. Mais dès que ton application a son propre code, une image publique ne suffit plus : il faut fabriquer une image qui contient CE code.

L'instruction qui change tout : build

C'est le rôle de l'instruction build. Au lieu de tirer une image depuis un registre distant, Compose la construit localement, à partir d'un Dockerfile présent dans ton projet. Le résultat est une image utilisable exactement comme les autres.

Une première version simple d'un Dockerfile

Un Dockerfile basique installe les dépendances, copie le code, puis démarre l'application. C'est suffisant pour débuter, mais cette approche a un défaut : l'image finale contient AUSSI tous les outils utilisés pour construire l'application, ce qui la rend inutilement lourde.

Il reste un problème : une image trop lourde

Une image de production qui traîne des outils de compilation est plus longue à transférer, et surtout plus risquée : plus il y a de logiciels installés, plus la surface d'attaque est grande. On aimerait ne garder que le résultat final.

La solution : le multi-stage build

C'est exactement ce que permet un Dockerfile "multi-stage". Imagine un chantier de construction : les échafaudages, les outils et les gravats (l'étage "builder") ne doivent jamais se retrouver dans la maison livrée au client (l'étage "runtime"). On copie uniquement le résultat fini d'un étage vers l'autre.

Choisir précisément quel étage construire

Une fois qu'on a plusieurs étages dans un même Dockerfile, le paramètre target permet de choisir explicitement lequel construire. C'est très pratique pour obtenir, à partir du même fichier, une image "dev" avec des outils de debug et une image "prod" minimale.

Étage (stage)ContenuUtilisé pour
builderOutils de compilation, code source complet, node_modules de devJamais livré tel quel
runtimeUniquement dist/ et les dépendances de productionImage livrée en production

Bonne pratique

Ordonne toujours tes instructions Dockerfile de la moins changeante à la plus changeante : copie package.json et lance npm ci AVANT de copier le reste du code source. Ainsi, tant que les dépendances ne changent pas, Docker réutilise le cache de cette étape et le build devient nettement plus rapide.

Les pièges à surveiller

Premier piège fréquent : oublier --build après avoir modifié le Dockerfile, ce qui fait que Compose réutilise silencieusement l'ancienne image en cache. Deuxième piège : mal ordonner les instructions COPY et RUN, ce qui empêche Docker d'exploiter son cache et ralentit chaque build. Enfin, sans fichier .dockerignore, des dossiers inutiles comme node_modules ou .git peuvent se retrouver copiés dans l'image par erreur.

Maintenant que tu sais construire tes propres images, la leçon suivante aborde un besoin différent : faire persister des données au-delà de la vie d'un conteneur, avec les volumes.

Commandes & code

Build depuis un Dockerfile

build remplace ou complète image pour construire l'image localement au lieu de la tirer d'un registre.

yaml
services:
  api:
    build: ./api                 # cherche ./api/Dockerfile
    image: myorg/api:dev          # tag optionnel donné à l'image construite
    ports:
      - "3000:3000"

  worker:
    build:
      context: ./worker
      dockerfile: Dockerfile.prod
      args:
        NODE_ENV: production
        BUILD_VERSION: "1.4.2"
      target: runtime             # stage ciblé dans un Dockerfile multi-stage
      cache_from:
        - myorg/worker:cache
dockerfile
# ./api/Dockerfile - multi-stage utilisé par "target: runtime"
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]
bash
# Forcer la reconstruction des images avant de démarrer
docker compose up -d --build

# Reconstruire une image sans démarrer les conteneurs
docker compose build --no-cache api

# Construire toutes les images en parallèle
docker compose build --parallel

Résumé

  • build: ./chemin construit depuis un Dockerfile local.
  • args passe des ARG de build, target choisit un stage multi-stage.
  • image: avec build: donne un tag à l'image construite.
  • --build force la reconstruction avant up.

Exercices pratiques

1 disponible
1

Mission : un Dockerfile à trois étages qui fuite en production

Objectif : Cibler le bon stage d'un Dockerfile multi-stage et réordonner les instructions pour exploiter le cache.

Contexte

Le Dockerfile de boutique-api a évolué : en plus de builder et runtime, un troisième stage debug (avec curl, bash et vim) a été ajouté pour le dépannage local. Le fichier docker-compose.prod.yml ne précise aucun target dans son bloc build:. Par ailleurs, un collègue place COPY . . avant RUN npm ci, ce qui rend chaque build de plus en plus lent.

Diagnostique puis corrige ces deux problèmes.

Résoudre l’exercice →