Aller au contenu

Docker Compose : arrêtez de mettre latest dans vos fichiers

Cryptolab.re
Auteur
Cryptolab.re
Cryptolab est un blog personnel où je documente mes expérimentations techniques : infra, self-hosting, réseau, crypto et projets parfois inutiles, souvent instructifs.
Sommaire

Le tag latest est confortable.

On écrit un compose.yml, on lance docker compose up -d, et le service démarre. Pas besoin de choisir une version de PostgreSQL, Redis, Traefik, Gitea, Vaultwarden ou n’importe quelle application auto-hébergée.

Le problème, c’est que latest ne veut pas dire “dernière version stable adaptée à mon environnement”. Ça veut seulement dire : “ce tag pointe vers quelque chose dans le registre au moment où Docker le résout”.

Ce quelque chose peut changer sans que votre fichier Compose change.

Pour une stack de test, ce n’est pas très grave. Pour une base de données, un reverse proxy exposé, un service d’authentification ou une application avec des volumes persistants, c’est une mauvaise convention d’exploitation.

Le vrai sujet n’est pas Docker. C’est la reproductibilité.

En bref
#

  • latest est un tag mutable. Le mainteneur de l’image peut le faire pointer vers une autre image.
  • Une image sans tag explicite, comme postgres, revient en pratique à tirer postgres:latest.
  • Docker Compose accepte les tags (postgres:16) et les digests (postgres:16@sha256:...).
  • Le risque principal n’est pas uniquement la sécurité. C’est surtout l’imprévisibilité des redéploiements.
  • Une stack Compose exploitable doit dire clairement quelle version elle attend.
  • En homelab, un tag de version explicite suffit souvent. Pour les services critiques, un digest devient pertinent.
  • Les mises à jour doivent être visibles dans Git, relues, testées et réversibles.

Ce que latest veut dire
#

Dans Docker, un tag est un pointeur lisible vers une image.

Ce pointeur peut changer. Docker le documente explicitement : les tags d’image sont mutables. Un éditeur peut donc publier une nouvelle image derrière un tag déjà connu.

C’est pratique pour distribuer automatiquement les derniers patchs d’une série. C’est beaucoup moins sain quand ce comportement devient implicite dans votre infra.

Dans Compose, ces deux formes sont problématiques :

services:
  db:
    image: postgres:latest
services:
  db:
    image: postgres

La seconde est souvent oubliée. Si aucun tag n’est fourni, Docker utilise :latest par défaut.

La forme minimale correcte ressemble plutôt à ceci :

services:
  db:
    image: postgres:16

Et la forme la plus reproductible garde un digest :

services:
  db:
    image: postgres:16@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef

Le digest ci-dessus est un exemple. Dans une vraie stack, il faut utiliser le digest réel fourni par le registre.

Ce que latest casse vraiment
#

latest ne casse pas toujours immédiatement. C’est justement pour ça que l’habitude reste.

Le problème apparaît au mauvais moment :

  • après un redémarrage de serveur ;
  • pendant une migration vers une nouvelle machine ;
  • au moment de restaurer une sauvegarde ;
  • après un docker compose pull lancé machinalement ;
  • quand Watchtower ou un équivalent recrée un conteneur ;
  • quand un collègue ou vous-même relancez la stack six mois plus tard.

Prenons un exemple classique :

services:
  app:
    image: gitea/gitea:latest
    restart: unless-stopped
    volumes:
      - ./gitea:/data

  db:
    image: postgres:latest
    restart: unless-stopped
    volumes:
      - ./postgres:/var/lib/postgresql/data

Ce fichier ne raconte pas l’état attendu de la stack. Il raconte une intention vague : “prendre ce qui est considéré latest au moment du pull”.

En exploitation, ça pose plusieurs problèmes concrets :

  • la version exacte n’est pas visible dans le dépôt ;
  • un rollback ne peut pas simplement consister à revenir au commit précédent ;
  • une restauration peut démarrer avec une version différente de celle qui a produit les données ;
  • une migration applicative peut se déclencher sans que le changement soit explicite ;
  • une panne devient plus difficile à diagnostiquer, car le YAML n’a pas forcément bougé.

Ce n’est pas une question de purisme. C’est le genre de détail qui fait perdre du temps quand un service refuse de repartir après une maintenance.

latest et sécurité : le malentendu
#

L’argument habituel pour garder latest est simple :

Je veux recevoir les correctifs de sécurité.

C’est compréhensible, mais incomplet.

latest ne garantit pas que vos conteneurs déjà lancés sont corrigés. Un tag dans un fichier YAML ne met rien à jour tout seul. Il faut au minimum tirer l’image, recréer le conteneur et vérifier le service.

latest ne garantit pas non plus que le changement est limité à un correctif de sécurité. Vous pouvez récupérer :

  • un patch applicatif ;
  • une nouvelle version mineure ;
  • une version majeure, selon la politique du projet ;
  • une image reconstruite avec une base différente ;
  • une modification de comportement non liée à la CVE qui vous intéresse.

La bonne approche n’est donc pas “ne jamais mettre à jour”. Ce serait pire.

La bonne approche, c’est de rendre la mise à jour visible :

  1. identifier les images utilisées ;
  2. suivre les releases ou advisories des services importants ;
  3. modifier les tags ou digests dans Git ;
  4. lire les notes de version quand le service a de l’état ;
  5. sauvegarder avant les migrations risquées ;
  6. déployer ;
  7. vérifier les logs, l’état des conteneurs et les endpoints ;
  8. garder une version précédente connue.

latest saute surtout les étapes où quelqu’un regarde ce qui change.

Le comportement Compose à connaître
#

Docker Compose propose pull_policy pour contrôler quand une image est tirée.

Par défaut, quand l’image n’est pas construite localement, Compose tire l’image si elle n’existe pas déjà. La documentation précise aussi un point important : le tag latest est toujours tiré avec la politique missing.

Cette configuration reste donc plus mobile qu’elle n’en a l’air :

services:
  nginx:
    image: nginx:latest
    pull_policy: missing

En développement, ce comportement peut rendre service.

Sur une stack durable, il faut plutôt séparer deux intentions :

  • démarrer l’état déjà validé ;
  • mettre à jour volontairement.

Un déploiement explicite ressemble davantage à ceci :

docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100

Pour un service précis :

docker compose pull app
docker compose up -d --no-deps app
docker compose logs --tail=100 app

Ces commandes ne remplacent pas une vraie procédure de release. Mais elles ont une qualité importante : le changement est intentionnel.

Auditer une stack Docker Compose
#

Avant de corriger, il faut voir ce qui tourne.

Pour repérer les :latest explicites dans un dépôt :

rg -n 'image:\s*.*:latest' .

Si rg n’est pas disponible :

grep -RInE 'image:[[:space:]]*.*:latest' .

Pour repérer les images sans tag, regardez la configuration Compose résolue :

docker compose config

Cherchez les lignes image: qui ressemblent à postgres, redis, nginx ou gitea/gitea, sans :<tag> ni @sha256:<digest>.

Pour lister uniquement les images déclarées par la stack courante :

docker compose config --images

Pour comparer avec les conteneurs réellement lancés :

docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"

Pour voir les digests connus localement :

docker images --digests

Pour inspecter une image précise :

docker image inspect nginx:1.27

Sur les versions récentes de Compose, vous pouvez aussi produire une configuration avec digests résolus :

docker compose config --resolve-image-digests

Cette commande ne remplace pas une politique de mise à jour, mais elle aide à voir vers quel contenu les tags résolvent au moment de l’audit.

Choisir le bon niveau de pinning
#

Il n’y a pas une règle unique. Le niveau de précision dépend du risque opérationnel.

Type de serviceExemple raisonnableCommentaire
Test jetablenginx:latestacceptable si rien n’est persistant
Service interne peu critiqueredis:7simple, lisible, suit une série majeure
Base de donnéespostgres:16 ou plus précisjamais en latest pour une stack durable
Reverse proxy exposétraefik:v3.2éviter les surprises de configuration
Application self-hostedgitea/gitea:1.22.6version visible et rollback simple
Service critiqueimage:tag@sha256:...reproductibilité forte, maintenance nécessaire

Un tag de version majeure (postgres:16) est un compromis courant. Il permet de suivre les patchs d’une série sans passer silencieusement à PostgreSQL 17.

Un tag patch (gitea/gitea:1.22.6) donne un meilleur contrôle, mais demande de suivre plus activement les mises à jour.

Un digest est le plus strict. Docker permet de tirer une image par digest pour obtenir un contenu précis. En contrepartie, cette image ne recevra pas de mise à jour tant que le digest ne change pas.

Le digest est excellent pour la reproductibilité. Il est mauvais si vous l’utilisez comme excuse pour ne jamais patcher.

Exemple de correction
#

Avant :

services:
  app:
    image: gitea/gitea:latest
    restart: unless-stopped
    volumes:
      - ./gitea:/data

  db:
    image: postgres:latest
    restart: unless-stopped
    volumes:
      - ./postgres:/var/lib/postgresql/data

Après :

services:
  app:
    image: gitea/gitea:1.22.6
    restart: unless-stopped
    volumes:
      - ./gitea:/data

  db:
    image: postgres:16
    restart: unless-stopped
    volumes:
      - ./postgres:/var/lib/postgresql/data

Ce changement ne rend pas la stack parfaite. Il rend le contrat plus clair.

Si une mise à jour casse l’application, Git permet de voir quelle version était utilisée avant. Si vous migrez vers une autre machine, vous ne dépendez plus du sens actuel de latest.

Bases de données : soyez plus strict
#

Les bases de données méritent un traitement particulier.

Une image PostgreSQL, MariaDB, MySQL, MongoDB ou Redis n’est pas seulement un binaire. Elle manipule des données persistantes, parfois avec des formats, des extensions, des paramètres et des chemins de migration.

Avant une mise à jour de base de données :

  • vérifiez la version actuelle ;
  • lisez les notes de migration ;
  • prenez une sauvegarde ;
  • testez au moins une restauration sur un environnement séparé quand les données comptent vraiment ;
  • évitez les sauts de version majeure non préparés.

Exemple de sauvegarde PostgreSQL générique :

docker compose exec db pg_dumpall -U postgres > backup-postgres.sql

Adaptez le nom du service, l’utilisateur et la méthode à votre stack. Et gardez en tête qu’une sauvegarde non testée reste une hypothèse.

Watchtower, auto-update et fausse tranquillité
#

Les outils comme Watchtower peuvent être utiles. Ils deviennent dangereux quand ils masquent une absence de politique.

Mettre latest partout puis laisser un outil recréer automatiquement les conteneurs revient à accepter des changements non relus, non testés et parfois difficiles à corréler avec une panne.

Je l’éviterais pour :

  • bases de données ;
  • reverse proxies ;
  • services d’authentification ;
  • stockage ;
  • applications exposées publiquement ;
  • services avec migrations automatiques sensibles.

Un meilleur compromis consiste à automatiser les propositions de mise à jour, pas nécessairement leur application.

La nuance est importante : un outil qui informe ou ouvre une PR vous aide à garder le contrôle. Un outil qui redémarre tout seul peut transformer une mise à jour banale en panne difficile à dater.

Surveiller sans déployer automatiquement
#

Remplacer latest par des versions explicites ne veut pas dire surveiller les releases à la main tous les soirs.

Il faut séparer trois actions :

  1. détecter qu’une nouvelle image existe ;
  2. évaluer si elle vous concerne ;
  3. déployer quand vous avez choisi de le faire.

latest mélange les trois. C’est précisément ce qu’on veut éviter.

Option 1 : Diun pour être notifié
#

Diun fait une chose simple : surveiller des images de conteneurs et notifier quand un tag suivi change de digest.

C’est utile si vous voulez garder vos fichiers Compose simples, sans forcément mettre en place Renovate tout de suite.

Exemple minimal avec Docker :

services:
  diun:
    image: crazymax/diun:4
    command: serve
    restart: unless-stopped
    volumes:
      - ./diun:/data
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      - TZ=Europe/Paris
      - DIUN_WATCH_WORKERS=10
      - DIUN_WATCH_SCHEDULE=0 */6 * * *
      - DIUN_PROVIDERS_DOCKER=true
      - DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT=true

Cette configuration lit les conteneurs via le socket Docker et vérifie périodiquement les images. Sans DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT=true, Diun surveille seulement les conteneurs qui portent le label diun.enable=true.

Le point important : Diun ne redéploie pas. Il signale.

Ensuite, vous branchez une notification selon votre environnement : mail, Gotify, Matrix, Telegram, Slack, webhook ou autre. Le choix du canal importe moins que la discipline : une notification doit devenir une action traçable, pas un bruit de fond ignoré.

Il faut aussi comprendre sa limite : Diun est très pratique pour savoir qu’un tag que vous utilisez a changé. Pour proposer automatiquement de passer de gitea/gitea:1.22.6 à gitea/gitea:1.22.7, Renovate est plus adapté.

Option 2 : Renovate pour ouvrir des PR
#

Renovate est plus adapté si votre compose.yml est dans Git.

Il sait extraire les images depuis les fichiers Docker Compose classiques (compose.yml, docker-compose.yml, variantes .yaml) et proposer les mises à jour sous forme de pull requests.

Renovate le fait déjà par défaut sur les fichiers qui correspondent à compose*.yml ou compose*.yaml. Un renovate.json minimal peut donc rester très sobre :

{
  "extends": ["config:recommended"],
  "packageRules": [
    {
      "matchDatasources": ["docker"],
      "automerge": false
    }
  ]
}

L’intérêt n’est pas d’accepter toutes les PR automatiquement. L’intérêt est d’avoir :

  • un diff lisible ;
  • une version avant/après ;
  • un historique Git ;
  • un endroit naturel pour lire le changelog ;
  • un rollback évident si le changement casse quelque chose.

Pour une infra personnelle, c’est souvent le meilleur compromis : automatiser la veille, garder l’humain sur le déploiement.

Scanner les images avant de patcher
#

Les mises à jour d’images ne sont pas seulement une affaire de version applicative.

Une image contient aussi un système de base, des bibliothèques, parfois des binaires ajoutés par le mainteneur. Scanner l’image aide à repérer des vulnérabilités connues, même si ce n’est jamais une preuve que l’image est “sûre”.

Avec Trivy :

trivy image gitea/gitea:1.22.6

Pour se concentrer sur les vulnérabilités importantes :

trivy image --severity HIGH,CRITICAL gitea/gitea:1.22.6

Avec Docker Scout, si vous utilisez l’écosystème Docker correspondant :

docker scout cves gitea/gitea:1.22.6

Ces scanners sont utiles, mais il faut garder du recul :

  • ils dépendent de bases de vulnérabilités et de métadonnées paquet ;
  • ils peuvent produire des faux positifs ;
  • ils peuvent aussi manquer des problèmes ;
  • ils ne remplacent pas les advisories du projet.

Le bon usage est pragmatique : scanner pour prioriser, pas pour déléguer toute la décision à un score CVSS.

Une routine de mise à jour propre
#

Pour une stack Compose classique, une routine simple suffit souvent.

Commencez par regarder l’état actuel :

docker compose ps
docker compose config --images
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"

Modifiez ensuite les tags dans compose.yml, puis déployez :

docker compose pull
docker compose up -d
docker compose ps

Vérifiez les logs :

docker compose logs --tail=200

Si un service expose HTTP :

curl -I https://exemple.example

Pour un service critique, notez aussi la version avant et après quand l’image fournit une commande adaptée. Exemple avec Gitea :

docker compose exec app gitea --version

Cette dernière commande dépend évidemment de l’image. Certaines applications exposent une commande version, d’autres seulement une page d’administration. Le principe reste le même : ne vous contentez pas d’un conteneur “Up”.

Un workflow réaliste ressemble à ça :

  1. Diun ou Renovate signale une nouvelle version.
  2. Vous regardez le changelog si le service est important.
  3. Vous scannez l’image cible si elle est exposée ou sensible.
  4. Vous sauvegardez les données si une migration est possible.
  5. Vous modifiez le tag dans Git.
  6. Vous lancez docker compose pull.
  7. Vous redémarrez le service.
  8. Vous vérifiez les logs, l’endpoint et la version.
  9. Vous gardez le rollback dans Git.

Ce n’est pas lourd. C’est juste explicite.

Checklist rapide
#

Pour corriger une stack existante sans partir dans un chantier inutile :

  • remplacez toutes les images sans tag ;
  • remplacez tous les :latest durables ;
  • priorisez les bases de données et services exposés ;
  • utilisez au minimum une version majeure explicite ;
  • utilisez un tag patch ou un digest pour ce qui doit être strictement reproductible ;
  • gardez le fichier Compose dans Git ;
  • faites les mises à jour comme des changements visibles ;
  • documentez le rollback quand le service est important ;
  • automatisez les propositions, pas forcément les redémarrages.

La règle pratique tient en une phrase :

Si le service a des données ou reçoit du trafic réel, latest n’a rien à faire dans son fichier Compose.

Impact réel
#

Il faut rester nuancé.

latest dans un lab temporaire n’est pas un incident de sécurité. Pour tester une image pendant dix minutes, ce n’est pas le sujet.

Le risque augmente quand plusieurs conditions se cumulent :

  • service exposé sur Internet ;
  • données persistantes ;
  • migrations automatiques ;
  • sauvegardes non testées ;
  • redéploiement automatisé ;
  • absence d’historique Git ;
  • plusieurs personnes capables de relancer la stack.

Dans ce contexte, latest transforme une opération banale en changement implicite.

Et les changements implicites sont mauvais pour l’exploitation. Ils rendent les pannes moins lisibles, les retours arrière moins évidents, et les audits moins fiables.

Conclusion
#

latest est acceptable pour expérimenter. Il ne devrait pas être la convention par défaut d’une stack Docker Compose durable.

Utilisez des tags explicites. Pour les services sensibles, gardez un digest. Mettez à jour régulièrement, mais volontairement. Faites en sorte que le fichier Compose décrive l’état attendu, pas seulement une intention vague.

Ce n’est pas plus complexe. C’est juste plus honnête sur ce que vous exploitez réellement.

Sources
#

Articles connexes

opencode + Firecrawl : remplacer SearXNG pour l'IA locale

Dans mon setup opencode + Ollama sur RTX 3090, j’avais commencé avec SearXNG comme source web via MCP. Ça fonctionnait, mais ce n’était pas exactement le bon outil pour mon usage. SearXNG est un métamoteur de recherche. Il trouve des pages. Firecrawl est plus proche d’une brique d’extraction : il cherche, scrape, nettoie, crawl et renvoie du contenu exploitable par un agent. Pour un assistant local qui doit lire de la documentation, vérifier une API récente ou comparer plusieurs sources techniques, la différence se sent assez vite.

Assistant IA local : Ollama + opencode sur RTX 3090

J’utilise des LLMs comme assistants de code depuis début 2026. D’abord avec des API cloud, puis en local. Ce qui a changé avec une RTX 3090, c’est la bascule vers un modèle de travail où la latence et la confidentialité deviennent moins pénalisantes. L’inférence locale devient crédible pour du code à partir de 24 Go VRAM. Voici mon setup, les chiffres réels et ce qui tient vraiment la route.

Supply chain attacks : ce que Trivy, XZ et SolarWinds changent pour vos déploiements

Le 19 mars 2026, un groupe identifié comme TeamPCP a publié une version malveillante de Trivy, le scanner de vulnérabilités le plus utilisé du monde conteneurisé. L’attaque est exemplaire dans sa mécanique. TeamPCP a compromis le compte aqua-bot qui gère les releases de Trivy. Avec ce token, ils ont poussé un commit qui remplaçait actions/checkout par un fork malveillant, téléchargeant du code depuis un domaine typosquatté. Le flag --skip=validate de goreleaser était activé pour contourner la validation. En quelques minutes, 76 des 77 tags de version de trivy-action étaient force-pushés vers des commits infectés. Les 7 tags de setup-trivy étaient tous remplacés. Le payload était un credential stealer : dump de la mémoire du runner GitHub via /proc/<pid>/mem, sweep de 50+ chemins pour SSH keys, tokens AWS/GCP/Azure, secrets Kubernetes, Docker configs, fichiers .env, identifiants de base de données, wallets crypto. Les données étaient chiffrées en AES-256-CBC avec RSA-4096 et exfiltrées vers un serveur C2. Trois jours plus tard, des images Docker Hub v0.69.5 et v0.69.6 arrivaient avec le même payload. Et un ver npm, CanisterWorm, se propageait via le paquet containers-check. Aqua Security a détecté l’intrusion initiale fin février 2026 et avait rotationné des credentials. Mais la rotation n’était pas atomique. TeamPCP a gardé un accès résiduel via des tokens non révoqués. Le plus frappant n’est pas la sophistication. C’est la banalité du point d’entrée : un token CI mal rotationné. Et la confiance qu’on accorde à un outil qui est censé améliorer la sécurité.