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
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 :