Docker pour les devs - 31 - Docker en CI/CD

Builder, tester et déployer des images Docker dans GitLab CI et GitHub Actions sans exposer tes secrets.

  1. 01 Docker pour les devs - 00 - Pourquoi Docker change tout
  2. 02 Docker pour les devs - 01 - Containers vs VMs
  3. 03 Docker pour les devs - 02 - L'architecture de Docker
  4. 04 Docker pour les devs - 03 - Docker Desktop, Engine et alternatives
  5. 05 Docker pour les devs - 04 - Écrire un Dockerfile
  6. 06 Docker pour les devs - 05 - Layers et cache
  7. 07 Docker pour les devs - 06 - Le .dockerignore
  8. 08 Docker pour les devs - 07 - Multi-stage builds
  9. 09 Docker pour les devs - 08 - Choisir son image de base
  10. 10 Docker pour les devs - 09 - Docker Compose, les bases
  11. 11 Docker pour les devs - 10 - Docker Compose avance
  12. 12 Docker pour les devs - 11 - Networking Docker, les bases
  13. 13 Docker pour les devs - 12 - Networking Docker avance
  14. 14 Docker pour les devs - 13 - Volumes et persistance
  15. 15 Docker pour les devs - 14 - Variables d'environnement et secrets
  16. 16 Docker pour les devs - 15 - Permissions et utilisateurs
  17. 17 Docker pour les devs - 16 - Docker et monorepo
  18. 18 Docker pour les devs - 17 - Dev vs Prod
  19. 19 Docker pour les devs - 18 - ENTRYPOINT, CMD et scripts d'initialisation
  20. 20 Docker pour les devs - 19 - Debugger ses conteneurs
  21. 21 Docker pour les devs - 20 - Bases de donnees dans Docker
  22. 22 Docker pour les devs - 21 - Sauvegardes et restauration
  23. 23 Docker pour les devs - 22 - Conteneuriser un frontend
  24. 24 Docker pour les devs - 23 - Sécurité des conteneurs
  25. 25 Docker pour les devs - 24 - Optimisation des images
  26. 26 Docker pour les devs - 25 - Builds multi-platform
  27. 27 Docker pour les devs - 26 - Limiter les ressources de tes conteneurs
  28. 28 Docker pour les devs - 27 - Gerer les logs comme un adulte
  29. 29 Docker pour les devs - 28 - Healthchecks et restart policies
  30. 30 Docker pour les devs - 29 - Nettoyer Docker avant qu'il mange ton disque
  31. 31 Docker pour les devs - 30 - Registries et stratégie de tags
  32. 32 Docker pour les devs - 31 - Docker en CI/CD
  33. 33 Docker pour les devs - 32 - Au-dela de Compose
  34. 34 Docker pour les devs - 33 - Glossaire Docker de A a Z

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-action active BuildKit, nécessaire pour le cache avance
  • cache-from et cache-to avec type=gha utilisent 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_TOKEN est 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-from ou le cache GHA
  • GitHub Actions avec docker/build-push-action est le setup le plus simple
  • GitLab CI avec DinD fonctionne bien avec le cache registry
  • docker compose pull && up -d est 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

Sources

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