Tester son code, c’est prouver qu’il marche. L’emballer dans une image Docker, c’est garantir qu’il marchera partout pareil — sur le serveur de la coopérative comme sur celui d’à côté. L’étape qui relie les deux mondes, c’est la CI qui construit l’image automatiquement à chaque version et la dépose dans un registre, prête à être déployée. Dans ce tutoriel, vous conteneurisez l’API Récolte et vous la publiez sur ghcr.io, le registre intégré à GitHub, sans jamais lancer Docker sur votre machine.
Ce tutoriel fait partie de la série « CI/CD avec GitHub Actions ». Pour la vue d’ensemble, lisez d’abord le guide principal.
🎯 Ce que vous allez apprendre
- Écrire un
Dockerfilepropre pour une API Node.js. - Se connecter au registre
ghcr.iodepuis un workflow, sans mot de passe stocké, grâce auGITHUB_TOKEN. - Générer automatiquement des tags d’image cohérents avec
docker/metadata-action. - Construire et pousser l’image avec Buildx via
docker/build-push-action. - Accélérer les builds suivants avec le cache de couches intégré à GitHub Actions.
🛠️ Ce que vous allez construire
On part de l’API Récolte testée aux tutoriels précédents. On lui ajoute un Dockerfile, puis un workflow qui, à chaque push sur main et à chaque version taguée, construit l’image et la publie sur ghcr.io/votre-compte/recolte-api avec des tags propres. À la fin, n’importe quel serveur pourra récupérer l’image d’une seule commande — c’est précisément ce dont se servira le tutoriel de déploiement.
Prérequis
- Avoir suivi Secrets et environnements : vous connaissez le
GITHUB_TOKENet les permissions. - Des notions de Docker (image, conteneur,
Dockerfile). Sinon, lisez d’abord notre guide Docker pour débutants. - Test express : si vous savez ce qu’est une image de base et la différence entre
COPYetRUN, vous êtes prêt. - ⏱️ Temps estimé : ~45 minutes.
Étape 1 — Écrire le Dockerfile
Avant de penser à GitHub, il faut une recette d’image correcte. Un bon Dockerfile pour une API Node part d’une image de base légère, installe uniquement les dépendances de production, copie le code, puis lance le serveur. On soigne l’ordre des instructions pour profiter du cache de couches : ce qui change rarement (les dépendances) avant ce qui change souvent (le code).
# Dockerfile
FROM node:24-alpine
WORKDIR /app
# 1) On copie d'abord les manifestes, pour cacher l'install
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# 2) Puis le code source
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
L’image node:24-alpine est compacte (autour de 150 Mo sur disque) tout en embarquant Node 24. On copie package.json et le verrou avant le reste : tant qu’ils ne changent pas, Docker réutilise la couche npm ci sans réinstaller. Le --omit=dev exclut les dépendances de développement — inutiles en production et source de poids superflu. EXPOSE 3000 documente le port et CMD définit la commande de démarrage. Testez localement si Docker est installé (docker build -t recolte-api .) ; sinon, pas de panique, c’est le runner qui le fera.
Deux raffinements valent d’être pris dès maintenant, car ils évitent des mauvaises habitudes. D’abord, un fichier .dockerignore à côté du Dockerfile : il empêche de copier dans l’image des dossiers inutiles ou sensibles — exactement comme .gitignore pour Git.
# .dockerignore
node_modules
.git
.github
*.md
.env
Sans lui, le COPY . . embarquerait votre node_modules local (qui écraserait l’install propre du conteneur) et, pire, un éventuel fichier .env rempli de secrets. L’exclure est une question d’hygiène et de sécurité. Ensuite, un détail souvent oublié : par défaut, le processus tourne en root dans le conteneur. L’image officielle node fournit un utilisateur node non privilégié ; basculer dessus réduit la casse en cas de faille applicative.
COPY . .
USER node # on abandonne les droits root
EXPOSE 3000
CMD ["node", "server.js"]
Le principe est le même que pour les permissions du GITHUB_TOKEN au tutoriel précédent : on n’accorde que le strict nécessaire. Un conteneur qui n’a pas besoin d’être root ne doit pas l’être — c’est une couche de défense gratuite contre l’escalade de privilèges.
Étape 2 — Se connecter à ghcr.io (quick win)
GitHub héberge son propre registre d’images, ghcr.io (GitHub Container Registry), intégré à vos dépôts. L’énorme avantage : pas de compte tiers ni de mot de passe à gérer. Le GITHUB_TOKEN que vous connaissez déjà suffit à s’y connecter, à condition de lui accorder la permission packages: write. On utilise l’action officielle docker/login-action.
name: Image Docker
on:
push:
branches: [main]
tags: ['v*']
permissions:
contents: read
packages: write # autorise le push vers ghcr.io
jobs:
image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Se connecter à ghcr.io
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
Le déclencheur écoute les push sur main et les tags commençant par v (comme v1.0.0) : on construit l’image au fil du développement et à chaque version officielle. Le bloc permissions donne au jeton le droit d’écrire des paquets. login-action s’authentifie avec votre nom d’utilisateur (github.actor) et le GITHUB_TOKEN automatique — aucun secret manuel. Poussez : le step de connexion doit afficher Login Succeeded.
✅ Point d’étape — Le job doit se connecter à
ghcr.iosans erreur. Un échec denied signale presque toujours une permissionpackages: writeoubliée.
Étape 3 — Générer des tags cohérents automatiquement
Une image sans stratégie de tags devient vite un casse-tête : tout le monde écrase latest et plus personne ne sait ce qui tourne en production. docker/metadata-action résout ça en calculant des tags propres à partir du contexte Git — nom de branche, version sémantique, empreinte du commit — sans que vous ayez à les écrire à la main.
- name: Calculer les tags et labels
id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=sha,format=short
images indique le nom complet de l’image : ghcr.io/ suivi de github.repository (votre compte/dépôt). L’action met automatiquement ce nom en minuscules, comme l’exige un registre Docker. Les trois règles de tags se cumulent : type=ref,event=branch produit un tag au nom de la branche (main) ; type=semver transforme un tag Git v1.2.3 en tag d’image 1.2.3 ; type=sha,format=short ajoute l’empreinte courte du commit, idéale pour identifier sans ambiguïté l’image déployée. L’action expose le tout dans steps.meta.outputs.tags et steps.meta.outputs.labels, qu’on branche à l’étape de build.
Pourquoi tant de tags pour une seule image ? Parce qu’ils répondent à des questions différentes. Le tag de branche (main) sert au déploiement courant : « donne-moi la dernière version stable ». Le tag de version (1.2.3) sert à figer une release et à pouvoir y revenir. Le tag d’empreinte (sha-a1b2c3d) sert au diagnostic : quand un incident survient, il pointe vers le commit exact qui tourne, sans la moindre ambiguïté. Quant aux labels que produit aussi l’action, ce sont des métadonnées normalisées (standard OCI) gravées dans l’image — date de build, commit source, lien vers le dépôt. Un docker inspect sur le serveur révèle alors d’où vient précisément l’image, une traçabilité qui vaut de l’or le jour où l’on hérite d’un conteneur mystérieux en production.
Étape 4 — Construire et pousser l’image
Reste à assembler les morceaux. docker/setup-buildx-action active Buildx, le moteur de build moderne de Docker, qui gère le cache distant et le multi-plateforme. Puis docker/build-push-action construit l’image à partir du Dockerfile et la pousse avec les tags calculés.
- name: Préparer Buildx
uses: docker/setup-buildx-action@v4
- name: Construire et pousser
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: . indique où trouver le Dockerfile et le code. push: true envoie l’image au registre après le build (mettez false pour seulement construire, par exemple sur une pull request). tags et labels reprennent la sortie de l’étape précédente. Les deux lignes cache-from / cache-to avec type=gha sont la clé de la rapidité : elles stockent les couches de l’image dans le cache de GitHub Actions, si bien que les builds suivants ne reconstruisent que ce qui a changé. Le mode=max met en cache toutes les couches intermédiaires, pas seulement les finales.
Une question de bon sens se pose ici : faut-il construire une image à partir d’un code qui n’a pas passé les tests ? Non. On enchaîne donc proprement le job d’image sur le job de tests grâce au needs vu au tutoriel sur les secrets. Concrètement, dans le même workflow ou via un workflow dédié, le job image porte un needs: tests : il ne démarre que si la suite — y compris la matrice multi-versions — est verte. Vous obtenez ainsi une séquence saine : on teste, et seulement si tout passe, on fabrique l’artefact déployable. Une image publiée devient alors un gage de qualité, pas un simple paquet de code compilé à l’aveugle.
✅ Point d’étape — Après le push, ouvrez la page Packages de votre dépôt (ou votre profil) : l’image
recolte-apidoit y figurer, avec ses tagsmainet l’empreinte du commit. Relancez : le build doit être bien plus rapide, le journal indiquant des couches CACHED.
Étape 5 — Construire pour plusieurs architectures
Les serveurs ne partagent pas tous la même architecture processeur. La plupart des VPS sont en amd64, mais les machines ARM (certains serveurs économiques, un Raspberry Pi de test) se répandent. Buildx sait produire une image multi-plateforme en une passe, grâce à l’émulation fournie par docker/setup-qemu-action.
- name: Activer l'émulation multi-arch
uses: docker/setup-qemu-action@v4
- name: Préparer Buildx
uses: docker/setup-buildx-action@v4
# ... puis dans build-push-action :
- uses: docker/build-push-action@v7
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
La ligne platforms: linux/amd64,linux/arm64 demande à Buildx de produire les deux variantes et de les regrouper sous un même tag. Au moment du docker pull, chaque serveur récupérera automatiquement la variante correspondant à son processeur. C’est transparent pour qui déploie, et cela évite la mauvaise surprise d’une image qui refuse de démarrer sur une architecture inattendue. Le revers : un build multi-arch est plus lent (l’émulation a un coût), donc on le réserve souvent aux versions taguées plutôt qu’à chaque push.
🐞 Pièges fréquents
| Symptôme / erreur | Cause probable | Correctif |
|---|---|---|
denied: permission_denied au push |
Permission packages: write manquante |
Ajouter packages: write au bloc permissions |
invalid reference format: repository name must be lowercase |
Nom d’image en majuscules | Laisser metadata-action gérer la casse, ou minuscules manuelles |
| Le build réinstalle tout à chaque fois | COPY . . avant npm ci |
Copier package*.json d’abord, puis le code |
Le cache type=gha ne fonctionne pas |
Buildx non initialisé | Ajouter docker/setup-buildx-action avant le build |
| L’image est publique alors qu’on la voulait privée | Visibilité du paquet héritée du dépôt | Régler la visibilité dans Package settings |
🌍 Adaptation au contexte ouest-africain
Construire l’image côté GitHub change la donne quand la connexion locale est lente ou facturée au volume. Le téléchargement de l’image de base, la résolution des dépendances, la compilation éventuelle : tout se passe sur le runner, à pleine bande passante. Vous ne poussez que votre code source — quelques kilo-octets — et le runner fabrique une image de dizaines de méga-octets sans toucher à votre forfait Internet. Pour qui développe depuis Bamako ou Conakry avec une connexion capricieuse, c’est un gain de temps quotidien.
Le registre ghcr.io gratuit pour les dépôts publics évite aussi un abonnement à un registre tiers. Et l’image multi-architecture rend votre travail portable : le même tag tournera sur un VPS amd64 chez un hébergeur international comme sur une petite machine ARM hébergée localement, sans rebuild. Vous livrez une fois, vous déployez partout — un atout réel quand le parc de serveurs est hétérogène.
✅ Récapitulatif
Vous avez transformé du code testé en image prête à déployer, sans jamais quitter le pipeline. Vous savez écrire un Dockerfile qui exploite le cache de couches, vous connecter à ghcr.io avec le seul GITHUB_TOKEN, et générer des tags propres et traçables avec metadata-action. Vous construisez et poussez l’image avec Buildx, en accélérant les builds suivants grâce au cache type=gha, et vous savez produire une image multi-architecture quand le parc de serveurs l’exige. Il ne reste plus qu’à récupérer cette image sur un serveur — c’est l’objet du dernier tutoriel.
🧾 Aide-mémoire
| Élément | Rôle |
|---|---|
FROM node:24-alpine |
Image de base légère avec Node 24 |
npm ci --omit=dev |
Dépendances de production uniquement |
docker/login-action@v4 |
Connexion au registre ghcr.io |
docker/metadata-action@v6 |
Tags et labels automatiques |
docker/build-push-action@v7 |
Construire et pousser l’image |
cache-from / cache-to: type=gha |
Cache des couches dans GitHub Actions |
platforms: linux/amd64,linux/arm64 |
Image multi-architecture |
💪 À vous de jouer
1. Faites en sorte que l’image ne soit poussée que sur main et les tags de version, mais seulement construite (sans push) sur les pull requests, pour vérifier qu’elle compile.
2. Ajoutez un tag latest uniquement pour les versions taguées, pas pour chaque push de branche.
Voir une piste
- uses: docker/build-push-action@v7
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
# et dans metadata-action :
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
push devient une expression : faux sur une pull request, vrai sinon. Et la règle type=raw,value=latest,enable=… n’ajoute latest que lorsque la référence est un tag Git.
Tutoriels frères
- Secrets et environnements — le
GITHUB_TOKENet les permissions qui autorisent ce push. - Déploiement continu sur un VPS — récupérer et lancer l’image que vous venez de publier.
Pour aller plus loin
- 🔝 Retour au guide principal : GitHub Actions : le guide CI/CD pour bien démarrer
- Documentation officielle : docker/build-push-action et GitHub Container Registry.
- Étape suivante conseillée : le déploiement continu.
FAQ
Pourquoi ghcr.io plutôt que Docker Hub ?
Parce qu’il est intégré : pas de compte séparé, authentification par le GITHUB_TOKEN, et la même interface que vos dépôts. Docker Hub reste un excellent choix, mais demande un compte et un secret supplémentaires.
Mon image est-elle publique ou privée ?
Par défaut, elle suit la visibilité que vous lui donnez dans les réglages du paquet. Une image liée à un dépôt privé peut être rendue publique, et inversement. Vérifiez dans Package settings.
Faut-il reconstruire l’image à chaque commit ?
Pas forcément. Beaucoup d’équipes construisent à chaque push sur main pour la préproduction, et ne publient une image taguée latest ou versionnée qu’au moment d’une release. Le cache rend les builds fréquents peu coûteux.
Le multi-architecture ralentit-il beaucoup ?
Oui, l’émulation ARM sur un runner amd64 a un coût en temps. On le réserve souvent aux versions officielles, en gardant des builds amd64 rapides pour le quotidien.
Comment lancer l’image ensuite ?
Avec docker pull ghcr.io/votre-compte/recolte-api:main puis docker run, ou via Docker Compose sur le serveur. C’est exactement le sujet du tutoriel de déploiement.
Mots-clés : image Docker GitHub Actions, ghcr.io, docker build-push-action, docker login-action, metadata-action, cache build GitHub Actions, image multi-architecture, Buildx.