docker build && docker push depuis votre poste fonctionne tant que personne d’autre n’a besoin de publier la même image, ou tant que cela n’a pas à se produire à chaque merge sans que vous soyez devant le clavier. GitHub Actions exécute cette build sur les runners de GitHub, tague l’image selon ce qui a déclenché le workflow, et la publie sur un registry depuis lequel votre étape de déploiement peut faire un pull. Rien ne tourne en local.

Ce qu’il faut pour construire et publier une image Docker depuis la CI

Cinq actions doivent être en place avant qu’un runner puisse publier une image :

  • actions/checkout pour le contexte de build : votre Dockerfile et votre code source
  • docker/setup-buildx-action pour un builder BuildKit, car le Docker Engine de base du runner ne peut ni exporter le cache des layers ni construire pour d’autres plateformes
  • docker/login-action pour les identifiants du registry
  • docker/metadata-action pour transformer le ref Git en liste de tags et en labels OCI
  • docker/build-push-action pour exécuter la build et le push en un seul appel

Docker maintient les cinq, et elles sont conçues pour s’enchaîner : les sorties de l’étape metadata alimentent directement les entrées de l’étape build. En dessous, c’est le même docker build et docker push que vous lancez à la main, déplacé sur un runner et branché sur les événements Git.

Pourquoi le runner a besoin de Buildx avant de pouvoir construire quoi que ce soit

ubuntu-latest inclut Docker Engine, et Docker Engine construit des images sans problème avec docker build. Ce qu’il ne peut pas faire seul, c’est exporter un cache de build ailleurs que sur le disque local, ou construire pour une plateforme différente de celle du runner. docker/setup-buildx-action crée un builder Buildx adossé à BuildKit et le définit par défaut, ce qui rend possibles cache-to/cache-from et les builds multi-plateformes plus loin dans ce guide.

1
2
- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v4

Une seule étape, sans entrée nécessaire dans le cas courant. Si vous la sautez, build-push-action tourne quand même, avec le builder déjà présent sur le runner — la build réussit, mais l’export de cache et le multi-arch ne sont plus disponibles.

Comment s’authentifier sur GHCR avec le token intégré

Chaque exécution du workflow reçoit un GITHUB_TOKEN de courte durée, limité au dépôt dans lequel il tourne. Pour publier sur la GitHub Container Registry (ghcr.io), ce token suffit : pas de personal access token, pas de secret à créer ni à faire tourner.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
permissions:
  contents: read
  packages: write

steps:
  - name: Log in to GHCR
    uses: docker/login-action@v4
    with:
      registry: ghcr.io
      username: ${{ github.actor }}
      password: ${{ secrets.GITHUB_TOKEN }}

Le bloc permissions compte autant que l’étape de login. Sans packages: write, le token que GitHub remet au workflow peut lire mais pas publier, donc le login réussit et le push échoue avec un 403. Sur un dépôt d’organisation, il y a un second endroit à vérifier : un paramètre de permissions des workflows sous Settings → Actions → General peut limiter les tokens à la lecture seule, quoi que demande le fichier du workflow. Si le push échoue encore avec le bloc ci-dessus déjà en place, vérifiez ce paramètre.

Comment taguer les images automatiquement depuis le ref Git

Fixer un tag en dur dans le fichier du workflow oblige à modifier le YAML à chaque release. docker/metadata-action lit l’événement qui a déclenché l’exécution (quelle branche, quel tag, quelle PR) et produit la liste des tags et les labels OCI à votre place.

1
2
3
4
5
6
7
8
9
- name: Extract metadata
  id: meta
  uses: docker/metadata-action@v6
  with:
    images: ghcr.io/${{ github.repository }}
    tags: |
      type=semver,pattern={{version}}
      type=raw,value=latest,enable={{is_default_branch}}
      type=sha,format=short

Vous publiez un tag v1.4.0 et la règle semver produit 1.4.0. Vous poussez sur main et la règle raw ajoute latest. Chaque build reçoit un tag basé sur le short-SHA quel que soit le déclencheur, si bien qu’un commit précis reste récupérable même après que latest a avancé au-delà. steps.meta.outputs.tags est une liste de tags séparés par des sauts de ligne produite par ces trois règles pour cet événement, pas l’union des trois sur l’ensemble des exécutions.

À quoi ressemble le workflow complet

Assemblez les pièces et ajoutez docker/build-push-action, qui exécute docker build et docker push en un seul appel BuildKit :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
name: Build and push image

on:
  push:
    branches: [main]
    tags: ["v*.*.*"]

permissions:
  contents: read
  packages: write

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v7

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4

      - name: Log in to GHCR
        uses: docker/login-action@v4
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v6
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=semver,pattern={{version}}
            type=raw,value=latest,enable={{is_default_branch}}
            type=sha,format=short

      - name: Build and push
        uses: docker/build-push-action@v7
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

context: . construit depuis le dépôt tout juste récupéré en utilisant le Dockerfile qui s’y trouve ; pointez file: vers un autre chemin si le vôtre est ailleurs. Ce qui finit dans l’image reste l’affaire du Dockerfile, de l’ordre des layers à un USER non root. Ce workflow décide seulement quand la build tourne et où va le résultat.

Une chose à régler tout de suite : si la build elle-même a besoin d’un identifiant, par exemple un token pour un registry npm privé, passez-le par l’entrée secrets: de l’action plutôt que par un build-arg, qui reste visible dans l’historique de l’image. Variables d’Environnement et Secrets dans Docker détaille cette différence.

Comment mettre les layers en cache entre les exécutions du workflow

Un runner hébergé par GitHub démarre propre à chaque fois. Sans cache externe, chaque layer se reconstruit à chaque exécution, RUN npm ci compris. cache-from: type=gha et cache-to: type=gha,mode=max dans l’étape ci-dessus indiquent à BuildKit de lire et d’écrire son cache via le service de cache de GitHub Actions plutôt que sur le disque local, si bien que le cache survit au runner.

mode=max met en cache chaque layer intermédiaire, y compris ceux des étapes précédentes d’un Dockerfile multi-stage. Le défaut, mode=min, ne garde que les layers qui finissent dans l’image finale, ce qui veut dire que l’étape de build d’un Dockerfile multi-stage recompile de zéro à chaque exécution.

Le cache Actions a un plafond de taille par dépôt et évince les entrées les plus anciennes selon une logique LRU. Pour la plupart des images applicatives, cela ne pose jamais problème. Un dépôt qui construit plusieurs grandes images multi-arch peut l’atteindre, et un cache adossé à un registry (cache-to: type=registry,ref=ghcr.io/org/app:buildcache) est la solution, car il n’est pas plafonné par le quota du cache Actions.

Comment construire à chaque push mais ne publier qu’à la release

Le workflow ci-dessus publie à chaque push sur main et à chaque tag de version, ce qui est le bon défaut pour un projet qui déploie depuis main. Une configuration plus prudente construit et teste chaque pull request sans rien publier, et ne publie qu’après un merge :

1
2
3
4
5
6
7
8
9
on:
  pull_request:
  push:
    branches: [main]
    tags: ["v*.*.*"]

# dans l'étape build-push-action :
with:
  push: ${{ github.event_name != 'pull_request' }}

La build complète tourne quand même sur la PR, donc un Dockerfile cassé échoue avant le merge, tandis que push: false saute l’écriture sur le registry. Avec cache-to: type=gha déjà en place, cette build de PR prépare aussi le cache que le push post-merge réutilisera.

Ce qui change si vous publiez sur Docker Hub plutôt que GHCR

Docker Hub demande un argument de registry différent et des identifiants différents : un access token depuis Account Settings → Security, pas le mot de passe de votre compte, que Docker Hub n’accepte plus pour les connexions via API.

1
2
3
4
5
6
7
8
- name: Log in to Docker Hub
  uses: docker/login-action@v4
  with:
    username: ${{ vars.DOCKERHUB_USERNAME }}
    password: ${{ secrets.DOCKERHUB_TOKEN }}

# dans l'entrée images: de metadata-action, cela devient :
images: docker.io/yourusername/app

La configuration de Buildx, l’extraction des metadata et l’étape build-push ne changent pas. Le registry est un paramètre, pas un pipeline différent.

Ce que ce workflow ne couvre pas

Les images multi-plateformes (linux/amd64 plus linux/arm64 dans un seul manifest) nécessitent docker/setup-qemu-action avant Buildx et une entrée platforms: dans l’étape de build. L’émulation QEMU rend une build arm64 sur un runner amd64 lente, souvent plusieurs fois plus lente que du natif. Si vous construisez régulièrement en arm64, les runners arm64 hébergés par GitHub suppriment complètement l’émulation.

Les runners self-hosted qui ont déjà Buildx configuré n’ont pas besoin de setup-buildx-action, et un builder persistant là-bas peut battre le backend de cache Actions en gardant les layers sur disque local entre les exécutions. Un monorepo qui publie trois images de service passe généralement à une matrix de build plutôt qu’à trois copies de ce job, ce qui change la façon dont context et images sont paramétrés mais pas les étapes elles-mêmes.

Comment tester le workflow avant de taguer une release

Poussez d’abord sur une branche, pas sur un tag de version. Regardez l’exécution dans l’onglet Actions, puis vérifiez l’image sur la page Packages de votre dépôt, ou faites le pull directement avec docker pull ghcr.io/<owner>/<repo>:<short-sha> en utilisant le tag du log de l’exécution. Une fois ce pull réussi, une release est le même git tag v1.0.0 && git push --tags que vous utilisez déjà, et le workflow prend le relais à partir de là.

Articles associés