Déploiement automatique avec GitLab CI - 04 - Docker cache, rebuild et zero-downtime

Gerer le cache Docker dans un pipeline CI. Quand utiliser --no-cache, optimiser le Dockerfile, et le zero-downtime.

04 - Docker cache, rebuild et zero-downtime

Ce que tu vas apprendre

  • Comment le cache Docker fonctionne (et comment il peut te trahir)
  • Quand utiliser --no-cache et quand s'en passer
  • Comment ordonner ton Dockerfile pour un cache optimal
  • Ce qui se passe pendant un docker compose up -d en production

Prerequisites


Le problème qu'on a eu

Mercredi, 14h. Je pousse un fix CSS sur paltemps.fr. Le pipeline passe, tout est vert. Je rafraichis le site. Le vieux CSS est toujours la. Hard refresh. Toujours l'ancien. Je vide le cache navigateur. Toujours pareil.

J'ai perdu 45 minutes a croire que c'etait un problème de cache navigateur ou de Caddy. En fait, c'etait Docker.

Le Dockerfile copie les fichiers source, lance le build, et produit les assets statiques. Le problème : Docker cache chaque layer. Si le COPY . . détecté que les fichiers n'ont "pas change" (selon son algorithme de checksum), il réutilisé le layer cache. Sauf que parfois le checksum est le meme alors que le contenu a change. Concretement, des modifications dans des fichiers CSS imbriques ne declenchaient pas d'invalidation de cache.

La solution immediate : docker compose build --no-cache.

Comment le cache Docker fonctionne

Docker construit une image layer par layer. Chaque instruction du Dockerfile (FROM, COPY, RUN) créé un layer. Si une instruction et ses inputs n'ont pas change depuis le dernier build, Docker réutilisé le layer existant au lieu de le recalculer.

dockerfileFROM oven/bun:1 AS base        # Layer 1 - change rarement
WORKDIR /app                    # Layer 2 - change jamais

COPY package.json bun.lock ./   # Layer 3 - change quand les deps changent
RUN bun install --frozen-lockfile  # Layer 4 - idem

COPY . .                        # Layer 5 - change a chaque commit
RUN bun run build               # Layer 6 - recalcule si layer 5 change

Le cache s'invalide en cascade : si le layer 3 change, les layers 4, 5 et 6 sont recalcules. Mais si seul le layer 5 change, les layers 1 a 4 restent caches. C'est la theorie. En pratique, c'est presque toujours correct. Presque.

Quand le cache ment

Le cache peut servir du contenu obsolète dans quelques situations :

  • Les fichiers .dockerignore excluent des fichiers qui influencent le build
  • Des dépendances système ont ete mises à jour mais le RUN apt-get install est cache
  • Des fichiers générés (CSS compile, bundles JS) sont dans le contexte de build
  • Le build depend de variables d'environnement non declarees dans le Dockerfile

Pour un projet ou tu deploies 5 fois par jour et ou le build prend 30 secondes, --no-cache est la solution pragmatique. Tu perds 25 secondes de cache, tu gagnes la certitude que ton build est frais.

Les chiffres

J'ai mesure sur notre VPS (2 vCPU, 4 Go RAM) :

Type de build Duree Taille image
Avec cache (rien change) ~5 secondes 180 Mo
Avec cache (source change) ~15 secondes 180 Mo
Sans cache (--no-cache) ~45 secondes 180 Mo

45 secondes pour un déploiement complet, build inclus. Sur un projet plus gros avec des dépendances lourdes (node_modules de 500 Mo, build Webpack de 3 minutes), tu voudrais garder le cache pour les dépendances et ne rebuilder que le code source. Mais pour un projet Bun avec un build qui prend 10 secondes, --no-cache n'est pas un problème.

Optimiser l'ordre du Dockerfile

Meme si tu utilises --no-cache en CI, l'ordre de ton Dockerfile compte pour le développement local. La regle : mets les choses qui changent le moins souvent en premier.

dockerfile# 1. Image de base (change jamais)
FROM oven/bun:1 AS base
WORKDIR /app

# 2. Dependances (change quand tu ajoutes un package)
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile

# 3. Code source (change a chaque commit)
COPY . .
RUN bun run build

Si tu inverses et que tu mets COPY . . avant le RUN bun install, chaque modification de code invalide le cache des dépendances. Au lieu de 10 secondes de build local, tu te tapes 60 secondes a reinstaller les node_modules.

Ce qui se passe pendant `docker compose up -d`

Quand tu lances docker compose up -d apres un build, Docker Compose compare les images actuelles avec celles qui tournent. Si l'image d'un service a change, il arrêté l'ancien conteneur et en démarré un nouveau. Les services dont l'image n'a pas change continuent de tourner sans interruption.

Dans notre cas, seul le conteneur app est reconstruit a chaque deploy. Caddy, le serveur mail et Chrome ne bougent pas. Le temps d'arrêt du service app est d'environ 2 a 3 secondes : le temps entre l'arrêt de l'ancien conteneur et le démarrage du nouveau.

C'est du "near-zero-downtime". Pour du vrai zero-downtime, il faudrait un système de blue/green deployment ou de rolling update avec des health checks. Docker Compose seul ne fait pas ca. Mais 2 secondes de downtime a 3h du matin quand personne ne regarde ton site, franchement, c'est acceptable.

Le nettoyage : docker image prune

Chaque docker compose build créé une nouvelle image. L'ancienne n'est plus utilisee mais reste sur le disque. Apres 50 déploiements, tu as 50 images orphelines de 180 Mo chacune. Ca fait 9 Go de dechets.

bashdocker image prune -f

Le -f (force) évité la confirmation interactive. Cette commande supprime toutes les images "dangling" (celles qui n'ont plus de tag et ne sont utilisees par aucun conteneur).

Pour un nettoyage plus agressif (images, volumes, networks inutilises) :

bashdocker system prune -f

Attention avec docker system prune : ca supprime aussi les volumes non utilises. Si tu as des donnees dans des volumes anonymes, tu les perds. Pour notre pipeline, docker image prune -f suffit.

Mon avis pour ton projet

Si ton build total prend moins de 2 minutes : utilise --no-cache et ne te prends pas la tête. La fiabilité vaut plus que 30 secondes de build.

Si ton build prend plus de 5 minutes : optimise ton Dockerfile avec des multi-stage builds, séparé bien les dépendances du code source, et laisse le cache faire son travail. Mais vérifié régulièrement que les assets deploys sont bien à jour.


Navigation : Precedent : 03 - Écrire le .gitlab-ci.yml | Suivant : 05 - Troubleshooting


Sources

Retrouve d'autres articles techniques sur paltemps.fr.

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.