Pipeline CI/CD Node.js avec GitHub Actions et Docker

Automatiser la livraison d’une application Node.js permet de réduire les erreurs humaines et d’accélérer les mises en production. Avec GitHub Actions, chaque modification peut être vérifiée, empaquetée puis publiée selon un processus reproductible. A Guide To The Binary Heap And Its Priority Queue Applications.

Docker complète cette chaîne en fournissant un environnement identique entre le poste du développeur, le serveur d’intégration et l’infrastructure de production. Le conteneur devient ainsi l’unité de livraison, avec ses dépendances, sa version de Node.js et sa configuration de démarrage.

Un pipeline efficace ne se limite pas à lancer npm test. Il doit contrôler la qualité du code, construire une image légère, détecter les vulnérabilités et empêcher le déploiement lorsqu’une étape critique échoue.

L’exemple suivant s’appuie sur une API Node.js utilisant npm, un fichier Dockerfile et un dépôt GitHub. Les principes restent valables avec TypeScript, Express, NestJS ou un service basé sur Fastify.

Préparer l’application Node.js

Le projet doit posséder des scripts npm explicites afin que GitHub Actions sache quelles commandes exécuter. Une organisation courante comprend lint, test, build et éventuellement start :

{
  "scripts": {
    "lint": "eslint .",
    "test": "jest --runInBand",
    "build": "tsc",
    "start": "node dist/server.js"
  }
}

Les tests doivent être indépendants de la machine locale. Utilisez une base de données éphémère, des variables d’environnement dédiées et des dépendances verrouillées par package-lock.json. La commande npm ci installe exactement les versions validées et convient mieux à l’intégration continue que npm install.

La gestion des erreurs mérite aussi une vérification automatisée. Des tests unitaires couvrent les services et les fonctions métier, tandis que des tests d’intégration vérifient les routes HTTP, l’authentification et les interactions avec les systèmes externes.

Construire une image Docker fiable

Un Dockerfile multi-étape sépare la compilation de l’exécution. Cette approche réduit la taille de l’image finale et évite d’y inclure les outils de développement :

FROM node:20-alpine AS build
WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build
RUN npm prune --omit=dev

FROM node:20-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production

COPY --from=build /app/package*.json ./
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist

USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]

Ajoutez un fichier .dockerignore contenant notamment node_modules, .git, les fichiers de couverture et les secrets. L’image doit fonctionner avec un utilisateur non privilégié, exposer uniquement le port nécessaire et recevoir sa configuration via l’environnement.

Un contrôle de santé peut être ajouté avec une route /health. Le serveur doit répondre rapidement à cette route sans dépendre d’un traitement long. Cette vérification sera utile au déploiement et à l’orchestrateur chargé de redémarrer un conteneur défaillant.

Organiser les étapes du workflow

Dans .github/workflows/ci-cd.yml, le workflow peut se déclencher sur les demandes de fusion et sur la branche principale. Une première tâche installe Node.js, restaure le cache npm, exécute le lint et lance les tests :

name: CI/CD

on:
  pull_request:
  push:
    branches: [main]

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - run: npm ci
      - run: npm run lint
      - run: npm test
      - run: npm run build

Chaque étape doit échouer avec un code de sortie différent de zéro lorsque la qualité attendue n’est pas respectée. Il est préférable de conserver des tâches courtes et lisibles plutôt qu’un unique script opaque qui masque l’origine de l’erreur.

Les versions des actions doivent être maintenues et, dans un contexte sensible, épinglées sur un commit vérifié. Les permissions du jeton GitHub doivent rester minimales, par exemple avec contents: read pour les étapes qui ne publient rien.

Comparer les stratégies de publication

Le choix du registre et du mode de déploiement influence la sécurité, le coût et la vitesse du pipeline. Une image peut être envoyée vers GitHub Container Registry, Docker Hub ou un registre privé fourni par un cloud public.

Stratégie Avantage principal Point de vigilance Usage adapté
GitHub Container Registry Intégration avec le dépôt et les permissions GitHub Gestion des droits du paquet Projets hébergés sur GitHub
Docker Hub Écosystème largement connu Limites et exposition des images publiques Prototypes et petits services
Registre privé cloud Accès intégré à l’infrastructure Coût et configuration IAM Applications professionnelles
Déploiement sur serveur avec SSH Mise en place rapide Gestion manuelle de la disponibilité Environnements simples
Kubernetes ou service managé Mise à l’échelle et reprise automatisées Complexité opérationnelle Applications à fort trafic

L’image doit recevoir un tag immuable lié au commit, comme sha-a1b2c3d, en plus d’un tag lisible tel que main. En production, le déploiement d’un digest ou d’un tag de commit évite qu’une image modifiée soit utilisée par inadvertance.

Pour des besoins de calcul ou de traitement de données, les équipes peuvent aussi comparer les possibilités de leur zone cloud avant de choisir un registre, une région et un service d’exécution.

Publier l’image avec GitHub Actions

Après la validation du code, une tâche Docker construit et publie l’image uniquement pour la branche principale. GitHub recommande l’action docker/login-action pour l’authentification et docker/build-push-action pour la construction :

  image:
    needs: quality
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/metadata-action@v5
        id: meta
        with:
          images: ghcr.io/${{ github.repository }}

      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

Le cache BuildKit peut réduire le temps de compilation, particulièrement lorsque les dépendances changent rarement. Il faut toutefois surveiller la taille du cache et supprimer les artefacts obsolètes selon la politique du registre.

Ne placez jamais une clé privée, un mot de passe de base de données ou un jeton dans le dépôt. Les secrets GitHub, les environnements protégés et les variables injectées par le gestionnaire de déploiement doivent contrôler les informations sensibles.

Ajouter sécurité et tests avancés

Une analyse des dépendances avec npm audit peut être exécutée à chaque demande de fusion, avec un seuil adapté au niveau de risque du projet. Un scanner d’image Docker complète ce contrôle en recherchant les vulnérabilités du système de base et des paquets installés.

Les tests end-to-end doivent s’exécuter contre un conteneur lancé avec la même configuration que l’environnement cible. Un scénario de démonstration, comme un essai de machines à sous, peut par exemple servir de domaine fictif pour vérifier la navigation, la gestion de session et la validation des entrées, sans utiliser de données réelles.

Ajoutez également une analyse de secrets et une vérification de la configuration Docker. Des outils comme Trivy, Gitleaks ou les contrôles de sécurité intégrés à GitHub permettent de bloquer une fusion avant que la faille atteigne le registre.

Déployer sans interrompre le service

Le déploiement peut être déclenché après la publication de l’image, au moyen d’un webhook, d’un appel à une API cloud ou d’une connexion à un orchestrateur. Le serveur récupère alors l’image correspondant au commit validé, arrête l’ancienne version et démarre le nouveau conteneur.

Pour limiter les interruptions, utilisez une stratégie progressive : déploiement bleu-vert, remplacement contrôlé ou canary release. La nouvelle version reçoit d’abord une faible part du trafic, puis la proportion augmente si les indicateurs restent corrects.

Les environnements GitHub permettent de séparer préproduction et production, d’exiger une approbation manuelle et de définir des secrets propres à chaque contexte. Une équipe peut ainsi déployer automatiquement les branches de fonctionnalité en préproduction tout en protégeant la mise en ligne.

Surveiller et améliorer le pipeline

Un pipeline utile produit des journaux lisibles, conserve les rapports de tests et signale clairement l’étape responsable d’un échec. Les métriques de durée, de taux d’échec et de fréquence de déploiement aident à détecter un ralentissement ou une instabilité.

La supervision ne s’arrête pas après le démarrage du conteneur. Les logs structurés, les métriques HTTP, le suivi des erreurs et les alertes sur la mémoire permettent de distinguer une erreur applicative d’un problème d’infrastructure. Le contrôle /health doit vérifier l’état réel du service sans devenir une dépendance fragile.

La documentation doit expliquer le lancement local, les variables requises, la procédure de restauration et le fonctionnement des environnements. Elle peut aussi rappeler les règles de contribution et de revue, en s’appuyant sur des ressources communautaires lorsqu’une équipe souhaite formaliser ses pratiques de collaboration et d’inclusion.

Un pipeline CI/CD Node.js bien conçu transforme chaque commit en résultat vérifiable : code contrôlé, image reproductible, publication sécurisée et déploiement traçable. Commencez avec les tests et la construction Docker, puis ajoutez progressivement l’analyse des vulnérabilités, le registre privé et le déploiement automatisé. Placez le fichier workflow dans le dépôt, protégez la branche principale et mesurez chaque étape dès les premières exécutions.