31 - Docker en CI/CD
Ce que tu vas apprendre
- Comment builder des images dans une CI (GitLab CI, GitHub Actions)
- Les stratégies de cache pour accélérer les builds
- Comment push vers un registry de manière sécurisée
- Déployer avec docker compose pull + up
- Les bases du zero-downtime deploy
- Les erreurs de sécurité a éviter
Prerequisites
- Connaitre les registries et la stratégie de tags
- Avoir deja utilise une CI (GitLab CI ou GitHub Actions)
Mon premier pipeline Docker en CI prenait 12 minutes. Le build reconstruisait toutes les layers a chaque commit parce que le cache n'existait pas entre les jobs. Chaque npm install repartait de zero. Chaque apt-get install retelechergeait tout.
Apres avoir compris comment fonctionne le cache en CI, je suis passe a 2 minutes. La différence, c'est six heures gagnees par semaine pour une équipe de cinq devs.
GitHub Actions : un pipeline complet
Voici un workflow qui build, teste et push une image :
yamlname: Build and Push
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
Les points clés :
docker/setup-buildx-actionactive BuildKit, nécessaire pour le cache avancecache-frometcache-toavectype=ghautilisent le cache GitHub Actions. Les layers sont stockees entre les runs. Ca change tout.- On ne push que sur
main, pas sur les pull requests GITHUB_TOKENest fourni automatiquement, pas besoin de créer un secret
GitLab CI : un exemple réel
yamlstages:
- build
- test
- deploy
variables:
IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
IMAGE_LATEST: $CI_REGISTRY_IMAGE:latest
build:
stage: build
image: docker:24
services:
- docker:24-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker pull $IMAGE_LATEST || true
- docker build --cache-from $IMAGE_LATEST -t $IMAGE -t $IMAGE_LATEST .
- docker push $IMAGE
- docker push $IMAGE_LATEST
test:
stage: test
image: docker:24
services:
- docker:24-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker pull $IMAGE
- docker run --rm $IMAGE npm test
deploy:
stage: deploy
only:
- main
script:
- ssh deploy@prod "cd /app && docker compose pull && docker compose up -d"
L'astuce du docker pull $IMAGE_LATEST || true avant le build : ca telecharge la dernière image pour l'utiliser comme source de cache. Si elle n'existe pas (premier build), le || true évité que le pipeline echoue.
Stratégies de cache en CI
Le cache est le facteur numero un de performance en CI. Sans cache, chaque build repart de zero.
Cache registry : tu pull la dernière image et tu l'utilises comme cache.
bashdocker build --cache-from ghcr.io/monorg/mon-app:latest -t mon-app .
Ca marche avec les layers classiques. Simple mais limite : si une layer intermediaire change, tout ce qui suit est reconstruit.
Cache GHA (GitHub Actions) : le cache est stocke dans l'infrastructure GitHub. Rapide, transparent, et gere la TTL automatiquement.
yamlcache-from: type=gha
cache-to: type=gha,mode=max
Le mode=max exporte toutes les layers intermediaires, pas seulement celles du résultat final. Essentiel pour les multi-stage builds.
Cache inline : les metadonnees de cache sont embarquees dans l'image elle-meme.
bashdocker build --build-arg BUILDKIT_INLINE_CACHE=1 -t mon-app .
Moins performant que les autres méthodes mais fonctionne partout.
Déployer avec compose
La méthode la plus simple pour déployer : SSH + compose pull + up.
bashssh deploy@prod "cd /app && docker compose pull && docker compose up -d"
docker compose pull telecharge les nouvelles images. docker compose up -d relance les services dont l'image a change. Les services inchanges ne sont pas redemarres.
C'est ce que j'utilise sur paltemps.fr pour les projets simples. Ca tient la route tant que tu as un seul serveur et que tu acceptes quelques secondes d'interruption.
Zero-downtime deploy
Pour éviter l'interruption, la technique classique est le blue-green deploy :
yamlservices:
app-blue:
image: mon-app:1.0.0
# Ancien version, va etre supprimee
app-green:
image: mon-app:1.1.0
# Nouvelle version
nginx:
image: nginx
depends_on:
- app-green
# Pointe vers app-green
En pratique, avec Compose, c'est fastidieux a automatiser. Tu dois modifier la config nginx, relancer, vérifier que la nouvelle version répond, puis supprimer l'ancienne.
Une approche plus pragmatique : utiliser docker compose up -d --no-deps --scale app=2 pour lancer la nouvelle version en parallèle de l'ancienne, puis supprimer l'ancien conteneur. C'est un rolling update du pauvre, mais ca marche.
Pour du vrai zero-downtime, un reverse proxy comme Traefik avec des labels Docker simplifie beaucoup les choses. Ou tu passes a un orchestrateur (on en parle dans l'article sur ce qui vient apres Compose).
Sécurité en CI : les erreurs classiques
Ne mets jamais de secrets dans les build args :
dockerfile# JAMAIS CA
ARG DATABASE_URL
RUN echo $DATABASE_URL > /tmp/debug.log
Les build args sont visibles dans l'historique de l'image. N'importe qui avec docker history peut les lire.
Pour les secrets au build, utilise les secrets BuildKit :
dockerfileRUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm install
Ne push pas d'image avec des secrets : vérifié que ton .dockerignore exclut .env, les clés SSH, les fichiers de credentials.
Utilise des tokens temporaires : GITHUB_TOKEN dans GitHub Actions expire apres le job. C'est mieux qu'un Personal Access Token qui traine dans les secrets du repo.
Scanne tes images : ajoute un step de scan dans ton pipeline.
yaml- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
exit-code: "1"
severity: CRITICAL,HIGH
Ca bloque le pipeline si l'image contient des vulnérabilités critiques. Mieux vaut le savoir avant la prod qu'apres.
Résumé
- Le cache est la clé de la performance en CI : utilise
cache-fromou le cache GHA - GitHub Actions avec
docker/build-push-actionest le setup le plus simple - GitLab CI avec DinD fonctionne bien avec le cache registry
docker compose pull && up -dest suffisant pour déployer sur un serveur unique- Ne mets jamais de secrets dans les build args : utilise les secrets BuildKit
Precedent : 30 - Registries | Suivant : 32 - Au-dela de Compose