Aller au contenu

CI/CD avec GitHub Actions

Guide complet pour configurer le déploiement automatique vers staging et production.

Stratégie de Branches

┌─────────────────────────────────────────────────────────────────────────┐
│                         WORKFLOW CI/CD                                   │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   feature/* ──PR──► staging ──push──► Deploy VPS Staging                │
│                         │                                                │
│                         │                                                │
│                     PR + merge                                           │
│                         │                                                │
│                         ▼                                                │
│                       main ──push──► Deploy VPS Production              │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘
Branche Environnement VPS Label env
staging Staging VPS Staging (4 vCPU / 8GB) staging
main Production VPS Prod (6 vCPU / 12GB) prod

Secrets GitHub à Configurer

Configurez ces secrets dans chaque repository : Settings > Secrets and variables > Actions > Secrets

Secret Description Exemple
STAGING_SERVER_HOST IP du VPS Staging 185.123.xxx.xxx
STAGING_SERVER_USER User SSH staging deploy
STAGING_SSH_KEY Clé privée SSH staging -----BEGIN OPENSSH PRIVATE KEY-----...
PROD_SERVER_HOST IP du VPS Production 185.124.xxx.xxx
PROD_SERVER_USER User SSH production deploy
PROD_SSH_KEY Clé privée SSH prod -----BEGIN OPENSSH PRIVATE KEY-----...
LOKI_URL URL endpoint Loki https://loki.monitoring.lyroh.com/loki/api/v1/push
LOKI_USER Username Loki loki-push
LOKI_PASSWORD Password Loki votre_mot_de_passe

Secrets spécifiques à vos apps

Si vos applications ont besoin d'autres variables (DATABASE_URL, API_KEY, etc.), ajoutez-les selon vos besoins.


Workflow NestJS Backend

.github/workflows/deploy.yml

name: Deploy NestJS Backend

on:
  push:
    branches:
      - main
      - staging

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
  APP_NAME: nestjs-backend

jobs:
  # ════════════════════════════════════════════════════════════════════════
  # BUILD
  # ════════════════════════════════════════════════════════════════════════
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

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

      - name: Set variables
        id: vars
        run: |
          if [ "${{ github.ref_name }}" = "main" ]; then
            echo "tag=prod" >> $GITHUB_OUTPUT
          else
            echo "tag=staging" >> $GITHUB_OUTPUT
          fi

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.tag }}
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

  # ════════════════════════════════════════════════════════════════════════
  # DEPLOY STAGING
  # ════════════════════════════════════════════════════════════════════════
  deploy-staging:
    if: github.ref_name == 'staging'
    needs: build
    runs-on: ubuntu-latest
    environment: staging

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

      - name: Copy files to server
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.STAGING_SERVER_HOST }}
          username: ${{ secrets.STAGING_SERVER_USER }}
          key: ${{ secrets.STAGING_SSH_KEY }}
          source: "docker-compose.yml,docker-compose.staging.yml,alloy/"
          target: "/opt/${{ env.APP_NAME }}"

      - name: Deploy to Staging
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.STAGING_SERVER_HOST }}
          username: ${{ secrets.STAGING_SERVER_USER }}
          key: ${{ secrets.STAGING_SSH_KEY }}
          script: |
            cd /opt/${{ env.APP_NAME }}

            # Create .env
            cat > .env << 'ENVEOF'
            NODE_ENV=staging
            LOKI_URL=${{ secrets.LOKI_URL }}
            LOKI_USER=${{ secrets.LOKI_USER }}
            LOKI_PASSWORD=${{ secrets.LOKI_PASSWORD }}
            APP_ENV=staging
            HOST_NAME=$(hostname)
            ENVEOF

            # Login and deploy
            echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
            docker compose -f docker-compose.yml -f docker-compose.staging.yml pull
            docker compose -f docker-compose.yml -f docker-compose.staging.yml up -d --remove-orphans
            docker image prune -f

            echo "✅ Deployed to STAGING"

  # ════════════════════════════════════════════════════════════════════════
  # DEPLOY PRODUCTION
  # ════════════════════════════════════════════════════════════════════════
  deploy-prod:
    if: github.ref_name == 'main'
    needs: build
    runs-on: ubuntu-latest
    environment: production

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

      - name: Copy files to server
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.PROD_SERVER_HOST }}
          username: ${{ secrets.PROD_SERVER_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          source: "docker-compose.yml,docker-compose.prod.yml,alloy/"
          target: "/opt/${{ env.APP_NAME }}"

      - name: Deploy to Production
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.PROD_SERVER_HOST }}
          username: ${{ secrets.PROD_SERVER_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            cd /opt/${{ env.APP_NAME }}

            # Create .env
            cat > .env << 'ENVEOF'
            NODE_ENV=production
            LOKI_URL=${{ secrets.LOKI_URL }}
            LOKI_USER=${{ secrets.LOKI_USER }}
            LOKI_PASSWORD=${{ secrets.LOKI_PASSWORD }}
            APP_ENV=prod
            HOST_NAME=$(hostname)
            ENVEOF

            # Login and deploy
            echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
            docker compose -f docker-compose.yml -f docker-compose.prod.yml pull
            docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --remove-orphans
            docker image prune -f

            echo "✅ Deployed to PRODUCTION"

Workflow NextJS Apps

.github/workflows/deploy.yml

name: Deploy NextJS App

on:
  push:
    branches:
      - main
      - staging

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
  APP_NAME: nextjs-app-1  # Changer pour chaque app: nextjs-app-2, nextjs-app-3, etc.

jobs:
  # ════════════════════════════════════════════════════════════════════════
  # BUILD
  # ════════════════════════════════════════════════════════════════════════
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

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

      - name: Set variables
        id: vars
        run: |
          if [ "${{ github.ref_name }}" = "main" ]; then
            echo "tag=prod" >> $GITHUB_OUTPUT
          else
            echo "tag=staging" >> $GITHUB_OUTPUT
          fi

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.tag }}
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

  # ════════════════════════════════════════════════════════════════════════
  # DEPLOY STAGING
  # ════════════════════════════════════════════════════════════════════════
  deploy-staging:
    if: github.ref_name == 'staging'
    needs: build
    runs-on: ubuntu-latest
    environment: staging

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

      - name: Copy files to server
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.STAGING_SERVER_HOST }}
          username: ${{ secrets.STAGING_SERVER_USER }}
          key: ${{ secrets.STAGING_SSH_KEY }}
          source: "docker-compose.yml,docker-compose.staging.yml,alloy/"
          target: "/opt/${{ env.APP_NAME }}"

      - name: Deploy to Staging
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.STAGING_SERVER_HOST }}
          username: ${{ secrets.STAGING_SERVER_USER }}
          key: ${{ secrets.STAGING_SSH_KEY }}
          script: |
            cd /opt/${{ env.APP_NAME }}

            cat > .env << 'ENVEOF'
            NODE_ENV=staging
            APP_NAME=${{ env.APP_NAME }}
            LOKI_URL=${{ secrets.LOKI_URL }}
            LOKI_USER=${{ secrets.LOKI_USER }}
            LOKI_PASSWORD=${{ secrets.LOKI_PASSWORD }}
            APP_ENV=staging
            HOST_NAME=$(hostname)
            ENVEOF

            echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
            docker compose -f docker-compose.yml -f docker-compose.staging.yml pull
            docker compose -f docker-compose.yml -f docker-compose.staging.yml up -d --remove-orphans
            docker image prune -f

            echo "✅ Deployed to STAGING"

  # ════════════════════════════════════════════════════════════════════════
  # DEPLOY PRODUCTION
  # ════════════════════════════════════════════════════════════════════════
  deploy-prod:
    if: github.ref_name == 'main'
    needs: build
    runs-on: ubuntu-latest
    environment: production

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

      - name: Copy files to server
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.PROD_SERVER_HOST }}
          username: ${{ secrets.PROD_SERVER_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          source: "docker-compose.yml,docker-compose.prod.yml,alloy/"
          target: "/opt/${{ env.APP_NAME }}"

      - name: Deploy to Production
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.PROD_SERVER_HOST }}
          username: ${{ secrets.PROD_SERVER_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            cd /opt/${{ env.APP_NAME }}

            cat > .env << 'ENVEOF'
            NODE_ENV=production
            APP_NAME=${{ env.APP_NAME }}
            LOKI_URL=${{ secrets.LOKI_URL }}
            LOKI_USER=${{ secrets.LOKI_USER }}
            LOKI_PASSWORD=${{ secrets.LOKI_PASSWORD }}
            APP_ENV=prod
            HOST_NAME=$(hostname)
            ENVEOF

            echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
            docker compose -f docker-compose.yml -f docker-compose.prod.yml pull
            docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --remove-orphans
            docker image prune -f

            echo "✅ Deployed to PRODUCTION"

Workflow Python Scraper

.github/workflows/deploy.yml

name: Deploy Python Scraper

on:
  push:
    branches:
      - main
      - staging

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
  APP_NAME: python-scraper

jobs:
  # ════════════════════════════════════════════════════════════════════════
  # BUILD
  # ════════════════════════════════════════════════════════════════════════
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

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

      - name: Set variables
        id: vars
        run: |
          if [ "${{ github.ref_name }}" = "main" ]; then
            echo "tag=prod" >> $GITHUB_OUTPUT
          else
            echo "tag=staging" >> $GITHUB_OUTPUT
          fi

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.tag }}
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

  # ════════════════════════════════════════════════════════════════════════
  # DEPLOY STAGING
  # ════════════════════════════════════════════════════════════════════════
  deploy-staging:
    if: github.ref_name == 'staging'
    needs: build
    runs-on: ubuntu-latest
    environment: staging

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

      - name: Copy files to server
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.STAGING_SERVER_HOST }}
          username: ${{ secrets.STAGING_SERVER_USER }}
          key: ${{ secrets.STAGING_SSH_KEY }}
          source: "docker-compose.yml,docker-compose.staging.yml,alloy/"
          target: "/opt/${{ env.APP_NAME }}"

      - name: Deploy to Staging
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.STAGING_SERVER_HOST }}
          username: ${{ secrets.STAGING_SERVER_USER }}
          key: ${{ secrets.STAGING_SSH_KEY }}
          script: |
            cd /opt/${{ env.APP_NAME }}

            cat > .env << 'ENVEOF'
            PYTHONUNBUFFERED=1
            LOG_LEVEL=DEBUG
            LOKI_URL=${{ secrets.LOKI_URL }}
            LOKI_USER=${{ secrets.LOKI_USER }}
            LOKI_PASSWORD=${{ secrets.LOKI_PASSWORD }}
            APP_ENV=staging
            HOST_NAME=$(hostname)
            ENVEOF

            echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
            docker compose -f docker-compose.yml -f docker-compose.staging.yml pull
            docker compose -f docker-compose.yml -f docker-compose.staging.yml up -d --remove-orphans
            docker image prune -f

            echo "✅ Deployed to STAGING"

  # ════════════════════════════════════════════════════════════════════════
  # DEPLOY PRODUCTION
  # ════════════════════════════════════════════════════════════════════════
  deploy-prod:
    if: github.ref_name == 'main'
    needs: build
    runs-on: ubuntu-latest
    environment: production

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

      - name: Copy files to server
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.PROD_SERVER_HOST }}
          username: ${{ secrets.PROD_SERVER_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          source: "docker-compose.yml,docker-compose.prod.yml,alloy/"
          target: "/opt/${{ env.APP_NAME }}"

      - name: Deploy to Production
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.PROD_SERVER_HOST }}
          username: ${{ secrets.PROD_SERVER_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            cd /opt/${{ env.APP_NAME }}

            cat > .env << 'ENVEOF'
            PYTHONUNBUFFERED=1
            LOG_LEVEL=INFO
            LOKI_URL=${{ secrets.LOKI_URL }}
            LOKI_USER=${{ secrets.LOKI_USER }}
            LOKI_PASSWORD=${{ secrets.LOKI_PASSWORD }}
            APP_ENV=prod
            HOST_NAME=$(hostname)
            ENVEOF

            echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
            docker compose -f docker-compose.yml -f docker-compose.prod.yml pull
            docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --remove-orphans
            docker image prune -f

            echo "✅ Deployed to PRODUCTION"

Préparation des Serveurs

1. Créer l'utilisateur deploy

# Sur chaque VPS (staging et prod)
sudo adduser deploy
sudo usermod -aG docker deploy

2. Configurer SSH

# Sur votre machine locale

# Clé pour staging
ssh-keygen -t ed25519 -C "github-deploy-staging" -f ~/.ssh/deploy_staging
# → Copier le contenu de ~/.ssh/deploy_staging dans STAGING_SSH_KEY

# Clé pour production
ssh-keygen -t ed25519 -C "github-deploy-prod" -f ~/.ssh/deploy_prod
# → Copier le contenu de ~/.ssh/deploy_prod dans PROD_SSH_KEY

# Ajouter les clés publiques sur les serveurs
ssh-copy-id -i ~/.ssh/deploy_staging.pub deploy@IP_STAGING
ssh-copy-id -i ~/.ssh/deploy_prod.pub deploy@IP_PROD

3. Créer les dossiers sur les serveurs

# Sur VPS Staging
sudo mkdir -p /opt/{nestjs-backend,nextjs-app-1,nextjs-app-2,nextjs-app-3,nextjs-app-4,python-scraper}
sudo chown -R deploy:deploy /opt/

# Sur VPS Production (même chose)
sudo mkdir -p /opt/{nestjs-backend,nextjs-app-1,nextjs-app-2,nextjs-app-3,nextjs-app-4,python-scraper}
sudo chown -R deploy:deploy /opt/

Vérification dans Grafana

Après déploiement, vérifiez les logs :

# Logs staging
{env="staging"}

# Logs production
{env="prod"}

# Une app spécifique
{app="nestjs-backend", env="prod"}