SD
GitHub ActionsCI/CDNext.jsNestJSDevOps

CI/CD avec GitHub Actions : automatisez le déploiement de Next.js et NestJS

Mettez en place un pipeline CI/CD complet avec GitHub Actions pour déployer automatiquement votre app Next.js et votre API NestJS. Guide pratique pas à pas.

AMAlexis Mouchon9 min de lecture

Vous en avez assez de déployer votre application à la main à chaque modification ? Un pipeline CI/CD bien configuré, c'est la différence entre une livraison stressante et un simple git push qui fait tout le travail à votre place. Dans cet article, on met en place un workflow GitHub Actions complet pour une stack Next.js + NestJS.

Pourquoi automatiser ses déploiements ?

Le déploiement manuel, c'est une source d'erreurs humaines : oublier de lancer les tests, pousser sur la mauvaise branche, oublier de rebuilder l'image Docker. Un pipeline CI/CD élimine ces risques et apporte plusieurs avantages concrets :

Pour une stack Next.js + NestJS, on va distinguer deux workflows distincts : un pour le frontend, un pour le backend.

Architecture du projet et organisation des workflows

Partons d'une structure monorepo classique, avec le frontend et le backend dans le même dépôt GitHub :

mon-projet/
├── frontend/        # Next.js App Router
├── backend/         # NestJS
└── .github/
    └── workflows/
        ├── frontend.yml
        └── backend.yml

GitHub Actions détecte automatiquement les fichiers dans .github/workflows/ et les exécute selon les déclencheurs que vous définissez (push, pull_request, schedule, etc.).

Workflow CI/CD pour Next.js

Créez le fichier .github/workflows/frontend.yml :

name: Frontend CI/CD

on:
  push:
    branches: [main]
    paths:
      - 'frontend/**'
  pull_request:
    branches: [main]
    paths:
      - 'frontend/**'

jobs:
  lint-and-test:
    name: Lint & Tests
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./frontend

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: frontend/package-lock.json

      - name: Install dependencies
        run: npm ci

      - name: Run ESLint
        run: npm run lint

      - name: Run type check
        run: npm run type-check

      - name: Run tests
        run: npm run test -- --passWithNoTests

      - name: Build
        run: npm run build
        env:
          NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}

  deploy:
    name: Deploy to Vercel
    needs: lint-and-test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          working-directory: ./frontend
          vercel-args: '--prod'

Quelques points importants dans ce workflow :

paths : le workflow ne se déclenche que si des fichiers dans frontend/ ont été modifiés. Inutile de relancer le build du frontend si seul le backend a changé.

needs : le job deploy attend que lint-and-test soit passé avec succès. Si les tests échouent, le déploiement ne part jamais.

if : on ne déploie en production que sur un push sur main. Les pull requests déclenchent uniquement le lint et les tests.

Workflow CI/CD pour NestJS

Pour le backend, la logique est similaire mais le déploiement cible souvent un VPS ou un service comme Railway/Render. Voici un exemple avec une image Docker :

name: Backend CI/CD

on:
  push:
    branches: [main]
    paths:
      - 'backend/**'
  pull_request:
    branches: [main]
    paths:
      - 'backend/**'

jobs:
  lint-and-test:
    name: Lint & Tests
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./backend

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: backend/package-lock.json

      - name: Install dependencies
        run: npm ci

      - name: Run ESLint
        run: npm run lint

      - name: Run type check
        run: npm run build

      - name: Run unit tests
        run: npm run test

      - name: Run e2e tests
        run: npm run test:e2e
        env:
          MONGODB_URI: ${{ secrets.TEST_MONGODB_URI }}

  build-and-push:
    name: Build & Push Docker Image
    needs: lint-and-test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: ./backend
          push: true
          tags: |
            ${{ secrets.DOCKER_USERNAME }}/mon-backend:latest
            ${{ secrets.DOCKER_USERNAME }}/mon-backend:${{ github.sha }}

  deploy:
    name: Deploy to VPS
    needs: build-and-push
    runs-on: ubuntu-latest

    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_SSH_KEY }}
          script: |
            docker pull ${{ secrets.DOCKER_USERNAME }}/mon-backend:latest
            docker stop mon-backend || true
            docker rm mon-backend || true
            docker run -d \
              --name mon-backend \
              --restart unless-stopped \
              -p 3001:3001 \
              -e MONGODB_URI=${{ secrets.MONGODB_URI }} \
              -e JWT_SECRET=${{ secrets.JWT_SECRET }} \
              ${{ secrets.DOCKER_USERNAME }}/mon-backend:latest

Ce workflow en trois jobs enchaîne : tests → build de l'image Docker → déploiement SSH sur le VPS. Si l'une des étapes échoue, la chaîne s'arrête.

Gérer les secrets GitHub

Toutes les valeurs sensibles (tokens, clés SSH, URIs de base de données) doivent être stockées dans GitHub Secrets, jamais dans le code. Pour les configurer : Settings → Secrets and variables → Actions → New repository secret.

Pour une stack Next.js + NestJS, vous aurez typiquement besoin de :

# Frontend (Vercel)
VERCEL_TOKEN
VERCEL_ORG_ID
VERCEL_PROJECT_ID
NEXT_PUBLIC_API_URL

# Backend (Docker + VPS)
DOCKER_USERNAME
DOCKER_PASSWORD
VPS_HOST
VPS_USER
VPS_SSH_KEY
MONGODB_URI
JWT_SECRET
TEST_MONGODB_URI

Une bonne pratique : utilisez des secrets d'environnement distincts pour staging et production. GitHub permet de créer des environnements avec leurs propres secrets et des règles de protection (approbation manuelle avant déploiement en prod, par exemple).

Optimiser la vitesse du pipeline

Un pipeline lent devient vite frustrant. Voici trois techniques pour garder des temps de run raisonnables :

Cache des dépendances npm

L'option cache: 'npm' sur actions/setup-node met automatiquement en cache le répertoire node_modules entre les runs, en se basant sur le hash du package-lock.json. Un gain de 1 à 2 minutes sur les projets avec beaucoup de dépendances.

Cache Docker BuildKit

Pour les builds Docker, activez le cache des layers :

- name: Build and push Docker image
  uses: docker/build-push-action@v5
  with:
    context: ./backend
    push: true
    tags: mon-image:latest
    cache-from: type=gha
    cache-to: type=gha,mode=max

Jobs parallèles

Si vous avez des vérifications indépendantes (lint TypeScript, tests unitaires, tests e2e), lancez-les en parallèle plutôt qu'en séquence. GitHub Actions exécute des jobs sans needs simultanément.

Notifications et observabilité

Un pipeline CI/CD silencieux ne sert à rien si personne n'est alerté en cas d'échec. Ajoutez une notification Slack ou par email en fin de workflow :

- name: Notify on failure
  if: failure()
  uses: slackapi/slack-github-action@v1
  with:
    payload: |
      {
        "text": "❌ Déploiement échoué sur *${{ github.repository }}* (branche `${{ github.ref_name }}`)\nVoir les logs : ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
      }
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

La condition if: failure() garantit que cette étape ne s'exécute qu'en cas d'échec, évitant les notifications parasites.

Pour aller plus loin

Une fois ce pipeline en place, plusieurs améliorations sont possibles selon vos besoins : déploiements conditionnels par environnement (staging/prod), preview deployments automatiques sur les pull requests avec Vercel, matrix builds pour tester sur plusieurs versions de Node.js, ou encore intégration d'un scan de sécurité des dépendances avec npm audit.

L'automatisation du déploiement, c'est un investissement initial de quelques heures qui vous fait gagner du temps à chaque livraison — et surtout, vous enlève une source de stress à chaque mise en production.


Vous montez une stack Next.js + NestJS et vous voulez partir sur de bonnes bases dès le départ ? C'est exactement le genre de projet sur lequel j'interviens. Contactez-moi pour qu'on en discute.