SSD Nodes Learn 🎉 VPS dès $5.50/mois
Guides Matt ConnorPar Matt Connor

Docker Compose : build ou image sur un VPS ?

Dans Compose, image récupère un tag publié et build construit localement. Découvrez pourquoi compose up ignore votre modification du Dockerfile et la commande à utiliser.

Différence entre build et image dans Docker Compose : réponse courte

Dans un fichier Docker Compose, image: indique le nom d’une image à récupérer depuis un registry, tandis que build: demande à Compose d’en construire une localement à partir d’un Dockerfile. Si vous définissez uniquement image:, Compose récupère ce tag et l’exécute. Si vous définissez uniquement build:, Compose construit l’image localement et lui attribue un nom dérivé du nom du projet et de celui du service. Si vous définissez les deux, Compose construit l’image localement, puis lui attribue le nom indiqué dans image:. C’est ainsi que vous construisez une image et la poussez sous le nom de votre choix.

C’est toute la différence. La suite explique ce que cela implique en pratique sur un serveur. Ce tutoriel suppose que Docker Engine et le plugin Compose sont déjà installés ; exécuter Docker sur un VPS couvre cette partie.

Les trois formes en détail

Tirez une image publiée avec un tag, puis exécutez-la. Aucun Dockerfile n’intervient à aucun moment.

services:
  web:
    image: nginx:1.27
    restart: unless-stopped
    ports:
      - "80:80"

Construisez l’image à partir d’un Dockerfile situé dans le répertoire courant. Rien n’est téléchargé, à l’exception de l’image de base indiquée dans FROM.

services:
  web:
    build: .
    restart: unless-stopped
    ports:
      - "80:80"

Construisez l’image localement et attribuez un tag au résultat. docker compose push peut ensuite envoyer exactement ce tag vers un registry.

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    image: registry.example.com/acme/web:1.4.2
    restart: unless-stopped
    ports:
      - "80:80"

context est le répertoire envoyé au builder. dockerfile est résolu par rapport à ce contexte ; context: . avec dockerfile: docker/prod.Dockerfile est donc normal et correct. Exécutez docker compose images pour afficher le nom et l’ID de l’image utilisés par chaque conteneur de service. C’est le moyen le plus rapide de vérifier laquelle de ces trois formes vous avez réellement écrite.

Pourquoi docker compose up ne reconstruit-il pas l’image après la modification du Dockerfile ?

Parce que up vérifie si l’image existe, et non si elle est à jour.

Lorsque Compose démarre un service qui contient une section build:, il recherche l’image dans le stockage d’images local. Si une image portant ce nom existe déjà, Compose l’utilise. Il ne lit pas votre Dockerfile, ne compare pas vos fichiers source et ne vérifie aucun horodatage. La spécification Compose définit cette règle avec l’attribut pull_policy, et le comportement par défaut consiste à construire une image uniquement lorsqu’elle est absente. Une image présente est considérée comme suffisante.

Vous modifiez donc app.py, exécutez docker compose up -d, voyez Compose indiquer que le conteneur est en cours d’exécution, puis servez l’ancien code. Rien n’a échoué, donc aucun avertissement n’est affiché. C’est le cas le plus fréquent lorsque « ma modification n’a pas été prise en compte » avec Compose. L’indice se trouve dans le mot d’état que Compose affiche à côté du nom du conteneur : un conteneur remplacé par Compose est indiqué comme recréé ou démarré, tandis qu’un conteneur que Compose a décidé de laisser tel quel est indiqué comme en cours d’exécution.

Deux vérifications permettent de trancher. docker compose images affiche l’ID de l’image utilisée par chaque conteneur : notez-le avant le déploiement, puis comparez-le après. docker image ls contient une colonne CREATED ; une image créée avant votre dernier commit est obsolète, quel que soit le message affiché par le script de déploiement.

Quels flags forcent une reconstruction

  • docker compose up -d --build construit d’abord les images, puis recrée chaque conteneur dont l’image a changé. C’est le flag recherché dans la plupart des cas.
  • docker compose build web construit une seule service et ne démarre rien. Utilisez ensuite docker compose up --no-deps -d web pour remplacer uniquement ce conteneur et laisser le reste de la stack fonctionner.
  • docker compose build --no-cache web supprime toutes les layers mises en cache et reconstruit l’image depuis la première instruction.
  • docker compose build --pull tente de télécharger une version plus récente de l’image de base dans FROM. Un tag mutable tel que node:22 récupère ainsi son contenu actuel au lieu de réutiliser la copie téléchargée en mars.
  • docker compose up -d --force-recreate recrée les conteneurs à partir de l’image qu’ils utilisent déjà. Ce flag ne construit jamais d’image. L’utiliser alors que vous vouliez --build est une erreur fréquente.

Vous pouvez aussi définir ce comportement dans le fichier. Selon les termes de la spécification Compose, pull_policy: build signifie que Compose construit l’image et la reconstruit si elle existe déjà. Chaque up déclenche alors une construction, ce qui est souhaitable sur un laptop, mais rarement sur un serveur.

services:
  web:
    build: .
    image: registry.example.com/acme/web:dev
    pull_policy: build

Une autre interaction mérite d’être connue. docker compose pull tente également de télécharger les images des services qui possèdent une section build. Si ce téléchargement échoue, Compose indique que l’image doit être construite. Passez --ignore-buildable pour ignorer silencieusement ces services.

Comment le cache de build détermine la durée de votre déploiement

Chaque instruction d’un Dockerfile produit une layer, et le builder réutilise une layer en cache lorsque cette instruction et ses entrées n’ont pas changé. Pour COPY, les entrées sont le contenu des fichiers copiés. Dès qu’une layer n’est plus disponible dans le cache, toutes les layers suivantes sont reconstruites, car chacune est construite à partir du système de fichiers produit par la précédente.

Cette règle détermine à elle seule si votre déploiement prend quelques secondes ou plusieurs minutes. Ordonnez le Dockerfile des éléments qui changent rarement vers ceux qui changent à chaque commit.

FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]

npm ci se trouve après COPY . ., donc la modification d’un fichier source laisse la layer d’installation en cache et le build reprend à l’étape de copie. Si vous inversez ces deux lignes, une modification d’un seul caractère réinstalle toutes les dépendances, car COPY . . invalide la layer sur laquelle npm ci est construite. Le même principe s’applique à pip install -r requirements.txt et à go mod download.

--no-cache est l’outil adapté lorsque vous pensez qu’une layer obsolète masque votre correction. Ce n’est pas un bon choix par défaut, car cette option supprime la réutilisation que l’ordre du Dockerfile permet précisément d’obtenir.

Un élément peut être défini par l’image, puis remplacé par Compose : le CMD du Dockerfile correspond à la commande exécutée par défaut par l’image, et une clé command: dans le service le remplace. Interaction entre command et entrypoint est importante ici, car un override Compose peut faire se comporter une image fraîchement construite exactement comme l’ancienne.

Contexte de build et .dockerignore

context: . empaquette ce répertoire et l’envoie au builder avant l’exécution de la première instruction. Tout son contenu est inclus, notamment .git et tout répertoire de données que vous conservez à côté du code source. Si un build reste bloqué à l’étape de transfert du contexte alors que le projet n’a pas changé, cela signifie que le contexte est trop volumineux.

Un fichier .dockerignore à la racine du contexte exclut des chemins de ce transfert. Sa syntaxe est proche de celle de .gitignore.

.git
node_modules
*.log
data/
.env

Le gain est double. Le transfert est plus petit, donc chaque build démarre plus rapidement. De plus, COPY . . ne peut plus copier .env dans l’image, où toute personne qui récupère cette image peut ensuite le lire.

Le cas des builds qui ralentissent progressivement concerne les bind mounts. Un volume nommé se trouve en dehors du répertoire du projet, mais un bind mount comme ./data:/var/lib/postgresql/data se trouve dans le contexte de build. Les builds ralentissent donc chaque semaine à mesure que la base de données grossit. Une ligne dans .dockerignore suffit à corriger le problème. Bind mounts et volumes nommés présente les compromis plus généraux.

Les build arguments présentent une version moins importante du même risque. Les valeurs transmises via args: sont visibles dans l’historique de l’image par toute personne qui possède l’image. Placez-y donc un numéro de version, jamais un token. Fichiers env et secrets dans Compose indique plutôt où placer les identifiants.

Faut-il construire sur le VPS ou construire ailleurs puis récupérer l’image ?

Construire sur la machine qui sert votre trafic est le choix par défaut, car c’est le chemin le plus court : git pull, puis docker compose up -d --build. Cela convient sur un petit serveur dont personne ne dépend encore. Cette approche devient inadaptée pour deux raisons mesurables et une troisième qui n’apparaît que lors d’un incident.

Mémoire. Une build exécute les compilateurs et les bundlers à côté de votre application en production. Ce sont les composants les plus gourmands en mémoire de la plupart des stacks. Sur un VPS de 1 GB, un bundler JavaScript ou une compilation Rust est généralement le processus le plus volumineux de la machine. Lorsque le kernel n’a plus de mémoire, il tue le processus le plus volumineux : soit la build s’arrête avec Killed et le code de sortie 137, soit la base de données est tuée à sa place et le site tombe au milieu d’un déploiement. dmesg -T | grep -i oom affiche la ligne du kill avec le nom du processus. Vous pouvez donc identifier lequel des deux cas s’est produit au lieu de le supposer.

Disque. Chaque build laisse des layers, et le builder conserve son propre cache séparément de vos images. docker system df affiche les deux, et la ligne du build cache ne fait que grossir. Récupérez de l’espace avec docker image prune pour les images dangling et docker builder prune pour les layers en cache. Un disque plein ne bloque pas seulement la build. La base de données ne peut plus écrire non plus, et cette panne coûte bien plus cher qu’un déploiement lent.

Reproductibilité. Une image construite sur le serveur n’existe que sur ce serveur. Pour revenir en arrière, il faut checkout l’ancien commit et reconstruire l’image. Cette build ne garantit pas de produire le même résultat, car le tag de base a changé et les mirrors de packages ont également changé. Construire ailleurs puis pousser un tag transforme un rollback en simple modification : faites pointer image: vers le tag précédent, puis exécutez docker compose up -d.

L’organisation qui tient dans la durée est simple. Votre continuous integration exécute la build et pousse registry.example.com/acme/web:<git-sha>. Le fichier Compose du VPS contient image:, sans aucune clé build:. Le déploiement se limite alors à deux commandes qui utilisent presque aucune mémoire.

docker compose pull
docker compose up -d

Exécutez docker login registry.example.com une fois sur le serveur. Compose pourra ensuite récupérer les tags privés.

Conservez la section de build pour le développement au lieu de la supprimer, dans un fichier que vous nommez vous-même.

# compose.dev.yaml
services:
  web:
    build:
      context: .
    pull_policy: build
docker compose -f compose.yaml -f compose.dev.yaml up -d --build

Nommez ce fichier compose.dev.yaml, et non compose.override.yaml. Compose charge automatiquement un fichier d’override lorsqu’il est présent. Un override copié par erreur sur le serveur relancerait donc discrètement les builds sur cette machine. Superposer plusieurs fichiers Compose explique comment la fusion résout chaque clé.

Le piège d’architecture lors d’un build sur une autre machine

Une image conserve l’architecture CPU pour laquelle elle a été buildée. Si vous la buildez sur un laptop Apple Silicon, que vous la pushez, puis que vous récupérez ce tag sur un VPS x86_64, Docker signale que la plateforme de l’image demandée ne correspond pas à celle détectée sur l’hôte. Le processus s’arrête ensuite avec exec format error. Ce message ressemble à une erreur de binaire corrompu, mais ce n’est pas le cas. Indiquez explicitement la cible du build :

docker buildx build --platform linux/amd64 \
  -t registry.example.com/acme/web:1.4.2 --push .

Le même problème de correspondance se produit dans l’autre sens si votre laptop est x86 et que vous utilisez un VPS ARM plutôt qu’un VPS x86. Laissez la CI effectuer le build pour l’architecture sur laquelle vous déployez afin d’éviter ce problème.

Points à vérifier après un déploiement

  • docker compose images affiche l’image et le tag utilisés par chaque conteneur en cours d’exécution. Un ID d’image différent confirme que le nouveau build est actif.
  • docker compose config affiche le fichier fusionné après le remplacement des variables. Vous pouvez ainsi vérifier le nom d’image final que Compose utilisera avant d’exécuter quoi que ce soit.
  • docker compose logs -f web pendant la première demi-minute qui suit le basculement. Un conteneur qui démarre puis s’arrête redémarre en boucle au lieu de rester actif. Cette boucle reste silencieuse si vous ne la surveillez pas.
  • docker image ls affiche une colonne CREATED. Une image plus ancienne que votre dernier commit n’a jamais été reconstruite.

Si vous êtes encore en train de préparer le fichier concerné par ces vérifications, les bases d’un fichier Compose sur un VPS présente les clés associées, et l’aide-mémoire des commandes Compose répertorie les autres sous-commandes.

FAQ

Puis-je utiliser build et image dans le même service ?

Oui. C’est la configuration normale pour un projet que vous construisez vous-même. Compose construit l’image à partir de la section build: et lui attribue la valeur de image: comme tag. C’est ce tag que docker compose push envoie vers un registry et qu’une autre machine récupère. Sans clé image:, Compose construit quand même l’image, mais la nomme d’après le projet et le service. Il avertit alors que l’attribut manquant empêche le push de l’image.

Pourquoi docker compose up ne prend-il pas en compte ma modification du Dockerfile ?

Parce que up vérifie uniquement si une image portant ce nom existe. Si c’est le cas, Compose la démarre et ne compare jamais cette image au Dockerfile ni à vos fichiers source. Exécutez docker compose up -d --build, ou exécutez docker compose build web suivi de docker compose up --no-deps -d web pour remplacer un seul service. Définir pull_policy: build sur le service force un rebuild à chaque up, ce qui convient à une machine de développement.

Quelle est la différence entre --build et --force-recreate ?

--build reconstruit l’image, puis recrée les conteneurs dont l’image a changé. --force-recreate recrée les conteneurs à partir de l’image qu’ils utilisent déjà. Cette option ne peut donc jamais prendre en compte une modification du code. Si votre modification concerne les sources ou le Dockerfile, --build est le flag à utiliser. --force-recreate sert à réinitialiser le conteneur lui-même, par exemple pour vider sa couche inscriptible tout en conservant la même image.

Dois-je construire mes images Docker sur le VPS ou ailleurs ?

Construisez-les ailleurs et récupérez un tag une fois que le serveur sert également du trafic. Un build utilise la mémoire dont votre application a besoin. Sur un petit VPS, le kernel peut résoudre ce conflit en tuant le processus le plus gourmand, qui peut être le build ou la base de données. Les builds laissent également du cache sur le disque, sans mécanisme automatique pour le supprimer. Construire sur le serveur reste acceptable pour un petit projet sans utilisateurs. Une migration ultérieure est simple si vous conservez la section build: dans un fichier Compose réservé au développement.

Comment empêcher le cache de build Docker de remplir mon disque ?

Exécutez docker system df pour connaître l’espace occupé par vos images et par le cache de build. docker builder prune supprime les layers du cache et docker image prune supprime les images dangling laissées par les builds précédents. Ajouter -a à l’une ou l’autre commande est plus agressif et force le prochain build à repartir de zéro. Ne planifiez pas docker system prune -af --volumes sur un serveur, car --volumes supprime tous les volumes qu’aucun conteneur n’utilise actuellement. Or une stack arrêtée pour maintenance contient précisément sa base de données dans un tel volume.