Cloud & Déploiement angularforall.com

- GitHub Actions : déployer sur DigitalOcean en CI/CD

Github-Actions Digitalocean Ci-Cd Deploiement Ssh Droplet Rsync Pm2 Doctl Floating-Ip Devops Zero-Downtime Nginx Spaces
GitHub Actions : déployer sur DigitalOcean en CI/CD

Automatisez le déploiement sur un Droplet DigitalOcean avec GitHub Actions : SSH, rsync, secrets, rolling update et zero-downtime en production.

Pourquoi DigitalOcean + GitHub Actions ?

DigitalOcean s'est imposé comme le cloud favori des startups et des développeurs indépendants grâce à sa tarification prévisible, sa simplicité d'usage et son écosystème CLI mature (doctl). Couplé à GitHub Actions, vous obtenez un pipeline CI/CD complet sans louer de runner Jenkins ni payer de service tiers.

Là où AWS multiplie les services (CodePipeline, CodeDeploy, IAM roles, ECR…) pour un déploiement EC2, DigitalOcean reste minimaliste : un Droplet Ubuntu, une clé SSH, rsync, et c'est en ligne. Cette simplicité accélère le time-to-market — un MVP peut atteindre la production en moins de 30 minutes.

Comparaison : DigitalOcean vs AWS EC2 vs Hetzner

Critère DigitalOcean AWS EC2 Hetzner Cloud
VM 1 GB RAM / 1 vCPU 6 $/mois ~9 $/mois (t3.micro) 4,15 €/mois (CX22)
Bande passante incluse 1 To 1 Go (puis facturé) 20 To
CLI doctl simple aws-cli complexe hcloud simple
Stockage objet Spaces (5 $/mois 250 Go, S3-compatible) S3 (à l'usage) Object Storage (5,99 €/mois 1 To)
Plateforme managée App Platform (dès 5 $/mois) Elastic Beanstalk
Snapshots / Backups 0,06 $/Go/mois 0,05 $/Go/mois 20 % du prix VM
Floating IP Gratuite (si attachée) Elastic IP gratuite (si attachée) 1 €/mois
Verdict pragmatique : Hetzner offre le meilleur rapport prix/perfs en Europe. DigitalOcean l'emporte sur l'écosystème (App Platform, Spaces, Marketplaces, doctl). AWS reste pertinent dès que vous avez besoin de Lambda, RDS multi-AZ, CloudFront edge ou IAM granulaire.

Architecture cible de cet article

┌─────────────────┐       git push main      ┌──────────────────────┐
│  Développeur    │ ───────────────────────► │  GitHub repository   │
└─────────────────┘                          └──────────┬───────────┘
                                                        │ trigger
                                                        ▼
                                             ┌──────────────────────┐
                                             │  GitHub Actions      │
                                             │  - build & test      │
                                             │  - rsync over SSH    │
                                             └──────────┬───────────┘
                                                        │ SSH (port 22)
                                                        ▼
┌─────────────────┐    Floating IP (zero-DT) ┌──────────────────────┐
│  Utilisateurs   │ ◄───────────────────────►│  Droplet DigitalOcean│
└─────────────────┘                          │  Ubuntu 24.04 + Node │
                                             │  + PM2 + Nginx       │
                                             └──────────┬───────────┘
                                                        │
                                                        ▼
                                             ┌──────────────────────┐
                                             │  Spaces (assets/CDN) │
                                             │  + Snapshots backup  │
                                             └──────────────────────┘

Setup du Droplet et accès SSH

Un Droplet est l'équivalent DigitalOcean d'une instance EC2 : une VM Linux. Le plan basique à 6 $/mois (1 vCPU, 1 GB RAM, 25 GB SSD, 1 To bande passante) suffit pour 90 % des APIs et frontends statiques de petite envergure.

Création via doctl (CLI officielle)

# Installation doctl (Linux/macOS)
curl -L https://github.com/digitalocean/doctl/releases/download/v1.110.0/doctl-1.110.0-linux-amd64.tar.gz \
  | tar -xz && sudo mv doctl /usr/local/bin/

# Authentification (génère un token sur cloud.digitalocean.com/account/api/tokens)
doctl auth init --access-token "dop_v1_xxxxxxxxxxxxxxxxxxxxxx"

# Importer une clé SSH locale dans DigitalOcean
doctl compute ssh-key import deploy-key --public-key-file ~/.ssh/id_ed25519.pub

# Récupérer son ID de clé (nécessaire à la création du Droplet)
doctl compute ssh-key list
# ID          Name          FingerPrint
# 12345678    deploy-key    aa:bb:cc:dd:...

# Créer un Droplet Ubuntu 24.04 LTS dans la région Frankfurt (fra1)
doctl compute droplet create api-prod-01 \
  --image ubuntu-24-04-x64 \
  --size s-1vcpu-1gb \
  --region fra1 \
  --ssh-keys 12345678 \
  --enable-monitoring \
  --enable-backups \
  --tag-names prod,api,nodejs \
  --wait
L'option --enable-backups ajoute 20 % au coût mensuel (1,20 $/mois sur un Droplet 6 $) et conserve 4 snapshots hebdomadaires roulants. Indispensable en production : un rollback complet prend 5 minutes.

Configuration initiale serveur (Ubuntu 24.04)

# Connexion en root (uniquement la première fois)
ssh root@159.65.123.45

# Création d'un utilisateur dédié au déploiement
adduser --disabled-password --gecos "" deploy
usermod -aG sudo deploy

# Configuration sudo sans mot de passe pour les commandes systemd
echo 'deploy ALL=(ALL) NOPASSWD: /bin/systemctl restart api, /bin/systemctl status api' \
  > /etc/sudoers.d/deploy-api

# Copier la clé SSH du root vers deploy
mkdir -p /home/deploy/.ssh
cp /root/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys

# Durcissement SSH : interdire root, désactiver password auth
sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart ssh

# Firewall — autoriser uniquement SSH, HTTP, HTTPS
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable

# Installer Node.js 22 LTS, PM2, Nginx, certbot
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt-get install -y nodejs nginx certbot python3-certbot-nginx git
npm install -g pm2

# PM2 démarrera au boot sous l'utilisateur deploy
sudo -u deploy bash -c 'pm2 startup systemd -u deploy --hp /home/deploy'
Sécurité SSH critique : ne jamais autoriser PasswordAuthentication yes sur un serveur exposé. Les bots scannent en permanence le port 22 — passez aussi le port SSH sur 2222 ou 22022 pour réduire le bruit dans auth.log.

Génération d'une clé SSH dédiée au déploiement

# Sur votre machine locale — clé sans passphrase pour l'automation
ssh-keygen -t ed25519 -f ~/.ssh/github_actions_do -N "" \
  -C "github-actions@$(git config user.email)"

# Affiche la clé PUBLIQUE — à ajouter au Droplet
cat ~/.ssh/github_actions_do.pub

# Affiche la clé PRIVÉE — à coller dans le secret GitHub DO_SSH_KEY
cat ~/.ssh/github_actions_do
# Sur le Droplet — ajouter la clé publique
echo "ssh-ed25519 AAAAC3Nz...github-actions@..." \
  >> /home/deploy/.ssh/authorized_keys

# Tester depuis votre laptop
ssh -i ~/.ssh/github_actions_do deploy@159.65.123.45 "whoami"
# Doit afficher : deploy

Workflow GitHub Actions de base

Le workflow vit dans .github/workflows/deploy.yml à la racine du repo. Il est déclenché à chaque push sur main, lance les tests, build l'application et, si tout passe, déploie sur le Droplet.

Workflow Node.js + Express minimal

# .github/workflows/deploy.yml
name: Deploy to DigitalOcean

# Déclenche le pipeline sur push sur main + dispatch manuel
on:
  push:
    branches: [main]
  workflow_dispatch:

# Variables réutilisées par tous les jobs
env:
  NODE_VERSION: '22'
  REMOTE_HOST: ${{ secrets.DO_HOST }}
  REMOTE_USER: deploy
  REMOTE_PATH: /var/www/api

jobs:
  # JOB 1 : tests sur runner GitHub
  test:
    runs-on: ubuntu-24.04
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'  # cache automatique du dossier ~/.npm

      - name: Install dependencies
        run: npm ci --no-audit --no-fund

      - name: Lint
        run: npm run lint

      - name: Unit tests
        run: npm test -- --coverage

      - name: Upload coverage artifact
        uses: actions/upload-artifact@v4
        with:
          name: coverage-${{ github.sha }}
          path: coverage/
          retention-days: 7

  # JOB 2 : build + deploy (uniquement si test passe)
  deploy:
    needs: test
    runs-on: ubuntu-24.04
    # On ne déploie que sur main — protection contre push direct
    if: github.ref == 'refs/heads/main'
    environment:
      name: production
      url: https://api.example.com
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install & build
        run: |
          npm ci --no-audit --no-fund --omit=dev
          npm run build  # transpile TS -> dist/

      - name: Configure SSH agent
        uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${{ secrets.DO_SSH_KEY }}

      - name: Add Droplet to known_hosts
        run: |
          mkdir -p ~/.ssh
          ssh-keyscan -H ${{ env.REMOTE_HOST }} >> ~/.ssh/known_hosts

      - name: Deploy via rsync
        run: |
          rsync -avz --delete \
            --exclude='.git/' \
            --exclude='node_modules/' \
            --exclude='.env' \
            --exclude='coverage/' \
            ./ ${{ env.REMOTE_USER }}@${{ env.REMOTE_HOST }}:${{ env.REMOTE_PATH }}/

      - name: Install production deps & reload PM2
        run: |
          ssh ${{ env.REMOTE_USER }}@${{ env.REMOTE_HOST }} <<'EOF'
            set -euo pipefail
            cd /var/www/api
            npm ci --omit=dev --no-audit --no-fund
            pm2 reload ecosystem.config.cjs --update-env || pm2 start ecosystem.config.cjs
            pm2 save
          EOF

      - name: Health check post-deploy
        run: |
          sleep 5
          curl -fsS --max-time 10 https://api.example.com/health \
            || (echo "Health check failed" && exit 1)

Configuration PM2 (ecosystem.config.cjs)

// /var/www/api/ecosystem.config.cjs
// Gère plusieurs instances Node en cluster mode pour zero-downtime reload
module.exports = {
  apps: [{
    name: 'api',
    script: './dist/server.js',
    instances: 'max',          // 1 worker par CPU
    exec_mode: 'cluster',       // mode cluster (load balancing intégré)
    max_memory_restart: '400M', // restart si fuite mémoire
    env: {
      NODE_ENV: 'production',
      PORT: 3000,
    },
    error_file:  '/var/log/api/error.log',
    out_file:    '/var/log/api/out.log',
    merge_logs:  true,
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
  }],
};
pm2 reload (et non restart) recycle les workers cluster un par un — l'ancien worker continue de servir le trafic jusqu'à ce que le nouveau soit prêt. Aucune requête perdue tant qu'au moins 2 instances tournent.

Secrets GitHub et bonnes pratiques

Les secrets sont chiffrés au repos et exposés au workflow uniquement via ${{ secrets.NAME }}. Ils ne s'affichent jamais dans les logs (GitHub les masque automatiquement). Configurez-les dans Settings > Secrets and variables > Actions.

Liste des secrets indispensables

Nom Contenu Usage
DO_SSH_KEY Clé privée ed25519 complète (avec BEGIN/END) SSH agent
DO_HOST IP publique ou DNS du Droplet rsync, ssh
DO_API_TOKEN Token doctl (read+write) Floating IP, snapshots
SPACES_KEY / SPACES_SECRET Credentials Spaces (S3-compatible) Upload assets statiques
SLACK_WEBHOOK URL webhook entrant Slack Notifications déploiement

Environnements GitHub : protection production

# .github/workflows/deploy.yml — extrait
deploy:
  environment:
    name: production
    url: https://api.example.com
  # Les secrets définis sur l'environnement 'production' priment
  # sur les secrets repo + permettent required reviewers
  steps:
    - run: echo "Deploying to ${{ vars.ENV_NAME }}"
Bonne pratique : activez les required reviewers sur l'environnement production. Toute exécution du job nécessitera alors une approbation manuelle dans l'UI GitHub avant de toucher le serveur.

Synchroniser doctl dans le workflow

# Étape réutilisable pour avoir doctl disponible
- name: Install doctl
  uses: digitalocean/action-doctl@v2
  with:
    token: ${{ secrets.DO_API_TOKEN }}

- name: List active Droplets
  run: doctl compute droplet list --format ID,Name,PublicIPv4,Status

- name: Take pre-deploy snapshot
  run: |
    DROPLET_ID=$(doctl compute droplet list --format ID --no-header \
      --tag-name prod | head -n1)
    doctl compute droplet-action snapshot $DROPLET_ID \
      --snapshot-name "auto-pre-deploy-$(date +%Y%m%d-%H%M%S)" \
      --wait

OIDC : alternative sans clé persistante (avancé)

# GitHub peut émettre un JWT OIDC consommable par doctl 1.96+ pour générer
# des tokens DigitalOcean éphémères — élimine le secret long-lived.
# Documentation officielle : docs.digitalocean.com/products/oauth/oidc
permissions:
  id-token: write   # nécessaire pour OIDC
  contents: read

steps:
  - name: Authenticate to DigitalOcean via OIDC
    run: |
      OIDC_TOKEN=$(curl -fsSL -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
        "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://api.digitalocean.com")
      doctl auth init --oidc-token "$OIDC_TOKEN"

Déploiement rsync + PM2 / systemd

Trois stratégies de déploiement coexistent sur un Droplet : rsync (simple, fichiers), git pull (hooks post-receive), ou docker pull (image registry). Le rsync reste la solution la plus rapide pour un projet Node.js classique — pas de daemon Docker à provisionner.

Stratégie 1 : rsync + PM2 reload (recommandé)

# Bloc deploy détaillé du workflow
- name: Rsync to Droplet (dry-run check)
  run: |
    rsync -avzn --delete \
      --exclude-from='.deployignore' \
      ./ deploy@${{ env.REMOTE_HOST }}:${{ env.REMOTE_PATH }}/ \
      | tee rsync-plan.txt
    # -n = dry-run pour visualiser ce qui sera modifié

- name: Rsync to Droplet (apply)
  run: |
    rsync -avz --delete \
      --exclude-from='.deployignore' \
      --rsync-path='sudo rsync' \
      ./ deploy@${{ env.REMOTE_HOST }}:${{ env.REMOTE_PATH }}/

- name: PM2 reload remote
  run: |
    ssh deploy@${{ env.REMOTE_HOST }} \
      "cd ${{ env.REMOTE_PATH }} && \
       npm ci --omit=dev && \
       pm2 reload ecosystem.config.cjs --update-env"
# .deployignore — fichiers exclus du rsync
.git/
.github/
node_modules/
.env*
coverage/
*.log
.DS_Store
README.md
docs/
tests/
__tests__/

Stratégie 2 : git pull côté Droplet

# Le Droplet possède une clé deploy ajoutée comme "deploy key" sur GitHub
# Le workflow se contente de ssh + git pull
- name: Trigger remote git pull
  run: |
    ssh deploy@${{ env.REMOTE_HOST }} <<'EOF'
      set -euo pipefail
      cd /var/www/api
      git fetch origin main
      git reset --hard origin/main
      npm ci --omit=dev
      npm run build
      pm2 reload ecosystem.config.cjs
      pm2 save
    EOF
rsync vs git pull : rsync transmet les fichiers déjà buildés depuis le runner GitHub (CPU dédié, RAM dispo) — votre Droplet 1 GB n'a pas à compiler TypeScript. Le git pull pèse plus sur le serveur mais simplifie le rollback (git checkout v1.2.3).

Stratégie 3 : systemd unit (sans PM2)

# /etc/systemd/system/api.service
[Unit]
Description=API Node.js
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/api
ExecStart=/usr/bin/node /var/www/api/dist/server.js
Restart=always
RestartSec=5
StandardOutput=append:/var/log/api/out.log
StandardError=append:/var/log/api/error.log
Environment=NODE_ENV=production
Environment=PORT=3000
# Hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full

[Install]
WantedBy=multi-user.target
# Le workflow déclenche un restart systemd (sudoers configuré au setup)
- name: Restart systemd unit
  run: |
    ssh deploy@${{ env.REMOTE_HOST }} \
      "sudo systemctl restart api && sudo systemctl status api --no-pager"

Reverse proxy Nginx + TLS Let's Encrypt

# /etc/nginx/sites-available/api.conf
server {
    listen 80;
    server_name api.example.com;
    # Redirection 301 vers HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate     /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    # Limites de sécurité
    client_max_body_size 5M;
    add_header X-Frame-Options DENY always;
    add_header X-Content-Type-Options nosniff always;
    add_header Strict-Transport-Security "max-age=63072000" always;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Timeouts généreux pour SSE / long-polling
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
    }

    location /health {
        access_log off;
        proxy_pass http://127.0.0.1:3000/health;
    }
}
# Activer le vhost + obtenir le certificat
ln -s /etc/nginx/sites-available/api.conf /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
certbot --nginx -d api.example.com --non-interactive --agree-tos -m ops@example.com

Zero-downtime : Floating IP + blue-green

Un simple pm2 reload suffit pour la plupart des déploiements. Mais pour les applications à fort trafic, ou lorsque vous changez de version Node.js / système, la stratégie blue-green avec Floating IP élimine tout risque de coupure.

Principe

État initial :
  api-blue  (159.65.123.45)  ← Floating IP 138.197.10.20  ← trafic
  api-green (164.92.55.78)   ← idle

Déploiement :
  1. Push code vers api-green
  2. Healthcheck OK sur api-green
  3. Bascule Floating IP vers api-green via doctl
  4. api-blue devient idle (prochaine cible)

Rollback instantané :
  doctl compute floating-ip-action assign 138.197.10.20 BLUE_DROPLET_ID

Workflow avec bascule de Floating IP

# .github/workflows/blue-green.yml
name: Blue-Green Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4

      - name: Install doctl
        uses: digitalocean/action-doctl@v2
        with:
          token: ${{ secrets.DO_API_TOKEN }}

      - name: Configure SSH
        uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${{ secrets.DO_SSH_KEY }}

      # Détermine quel Droplet est INACTIF (cible du déploiement)
      - name: Detect inactive Droplet
        id: target
        run: |
          FLOATING_IP="${{ secrets.FLOATING_IP }}"
          ACTIVE_ID=$(doctl compute floating-ip get $FLOATING_IP \
            --format DropletID --no-header)
          BLUE_ID=$(doctl compute droplet list --tag-name blue --format ID --no-header)
          GREEN_ID=$(doctl compute droplet list --tag-name green --format ID --no-header)

          if [ "$ACTIVE_ID" = "$BLUE_ID" ]; then
            echo "target_id=$GREEN_ID"   >> $GITHUB_OUTPUT
            echo "target_color=green"     >> $GITHUB_OUTPUT
            echo "target_ip=$(doctl compute droplet get $GREEN_ID --format PublicIPv4 --no-header)" \
              >> $GITHUB_OUTPUT
          else
            echo "target_id=$BLUE_ID"    >> $GITHUB_OUTPUT
            echo "target_color=blue"      >> $GITHUB_OUTPUT
            echo "target_ip=$(doctl compute droplet get $BLUE_ID --format PublicIPv4 --no-header)" \
              >> $GITHUB_OUTPUT
          fi

      - name: Deploy to inactive Droplet
        run: |
          ssh-keyscan -H ${{ steps.target.outputs.target_ip }} >> ~/.ssh/known_hosts
          rsync -avz --delete ./ deploy@${{ steps.target.outputs.target_ip }}:/var/www/api/
          ssh deploy@${{ steps.target.outputs.target_ip }} \
            "cd /var/www/api && npm ci --omit=dev && pm2 reload ecosystem.config.cjs"

      # Healthcheck direct sur l'IP du Droplet (avant bascule)
      - name: Health check on inactive Droplet
        run: |
          for i in 1 2 3 4 5; do
            if curl -fsS --max-time 10 \
              http://${{ steps.target.outputs.target_ip }}/health; then
              echo "Health OK"
              exit 0
            fi
            sleep 5
          done
          echo "Health check failed after 5 retries"
          exit 1

      # Bascule de la Floating IP — coupure < 1 seconde
      - name: Switch Floating IP
        run: |
          doctl compute floating-ip-action assign \
            ${{ secrets.FLOATING_IP }} \
            ${{ steps.target.outputs.target_id }} \
            --wait

      - name: Notify Slack
        if: success()
        run: |
          curl -X POST -H 'Content-type: application/json' \
            --data '{"text":"Deployed to ${{ steps.target.outputs.target_color }} - https://api.example.com"}' \
            ${{ secrets.SLACK_WEBHOOK }}
Coût Floating IP : gratuite tant qu'elle est attachée à un Droplet. Elle devient facturée 0,006 $/h (~4 $/mois) si elle reste détachée — pratique pour réserver une IP entre deux déploiements.

App Platform — alternative sans gestion serveur

Pour qui ne veut pas gérer SSH, Nginx ni PM2, DigitalOcean propose App Platform (équivalent Heroku/Vercel). Le déploiement se déclenche directement depuis un push GitHub, sans workflow YAML.

# app.yaml — spec App Platform versionnée dans le repo
name: api-prod
region: fra
services:
  - name: api
    github:
      repo: monuser/api-repo
      branch: main
      deploy_on_push: true
    source_dir: /
    instance_size_slug: basic-xxs    # 5 $/mois — 512 MB RAM
    instance_count: 2                # auto load-balancing entre les 2
    http_port: 3000
    health_check:
      http_path: /health
      initial_delay_seconds: 10
      period_seconds: 10
    routes:
      - path: /
    envs:
      - key: NODE_ENV
        value: production
      - key: DATABASE_URL
        scope: RUN_TIME
        type: SECRET
        value: ${db.DATABASE_URL}
databases:
  - name: db
    engine: PG
    version: '16'
    size: db-s-dev-database         # 7 $/mois
Checklist zero-downtime production :
  • 2 Droplets minimum (blue + green) ou App Platform 2 instances
  • Floating IP attachée (gratuite si en usage)
  • Endpoint /health qui vérifie DB, Redis, dépendances critiques
  • PM2 cluster mode avec instances: 'max'
  • Healthcheck dans le workflow AVANT la bascule de Floating IP
  • Rollback testé : doctl compute floating-ip-action assign sur l'ID précédent

Monitoring DO, alertes et rollback

Un déploiement automatique sans monitoring est un déploiement aveugle. DigitalOcean offre DO Monitoring gratuit (CPU, RAM, disque, bandwidth, load) à activer sur chaque Droplet, plus des Alert Policies notifiables sur Slack ou e-mail.

Activer le monitoring agent

# Sur le Droplet — installation officielle
curl -sSL https://repos.insights.digitalocean.com/install.sh | sudo bash

# Vérifier que l'agent tourne
systemctl status do-agent

# Crée une Alert Policy via doctl (CPU > 80% pendant 5 min)
doctl monitoring alert create \
  --type "v1/insights/droplet/cpu" \
  --description "CPU > 80% sustained" \
  --compare GreaterThan \
  --value 80 \
  --window "5m" \
  --emails "ops@example.com" \
  --slack-channel "#alerts" \
  --slack-url "${{ secrets.SLACK_WEBHOOK }}" \
  --tags "prod"

Notifications de déploiement

# Étape post-deploy à ajouter au workflow
- name: Notify deployment outcome
  if: always()  # s'exécute même si une étape précédente échoue
  uses: 8398a7/action-slack@v3
  with:
    status: ${{ job.status }}
    fields: repo,message,commit,author,ref,workflow,took
    text: |
      Deploy ${{ job.status }} on ${{ env.REMOTE_HOST }}
      Commit: ${{ github.sha }}
      Author: ${{ github.actor }}
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

Rollback automatisé

# .github/workflows/rollback.yml — déclenché manuellement
name: Rollback Production

on:
  workflow_dispatch:
    inputs:
      target_color:
        description: 'Couleur cible (blue ou green)'
        required: true
        default: 'blue'
        type: choice
        options: [blue, green]

jobs:
  rollback:
    runs-on: ubuntu-24.04
    environment: production  # required reviewers actif
    steps:
      - name: Install doctl
        uses: digitalocean/action-doctl@v2
        with:
          token: ${{ secrets.DO_API_TOKEN }}

      - name: Switch Floating IP back
        run: |
          TARGET_ID=$(doctl compute droplet list \
            --tag-name "${{ inputs.target_color }}" \
            --format ID --no-header)
          doctl compute floating-ip-action assign \
            ${{ secrets.FLOATING_IP }} $TARGET_ID --wait

      - name: Verify
        run: |
          sleep 3
          curl -fsS https://api.example.com/health

Rollback via snapshot DigitalOcean

# Si le code est OK mais le Droplet est corrompu (DB locale, fichiers altérés)
# Restaurer depuis le snapshot pré-déploiement pris au début du workflow

# Lister les snapshots récents
doctl compute snapshot list --resource droplet \
  --format ID,Name,Created --filter "Name=auto-pre-deploy"

# Créer un Droplet depuis le snapshot
doctl compute droplet create api-rollback \
  --image <snapshot-id> \
  --size s-1vcpu-1gb \
  --region fra1 \
  --ssh-keys 12345678 \
  --wait

# Basculer la Floating IP sur le nouveau Droplet
NEW_ID=$(doctl compute droplet list --tag-name api-rollback \
  --format ID --no-header)
doctl compute floating-ip-action assign \
  138.197.10.20 $NEW_ID --wait
Métriques à surveiller en priorité : latence p95 de /health, taux d'erreur 5xx Nginx (tail -F /var/log/nginx/error.log), CPU/RAM Droplet, taille de la file PM2 (pm2 monit). Une dégradation post-deploy se voit dans les 60 secondes.

Logs centralisés vers Spaces (économique)

# Cron quotidien sur le Droplet — archive les logs PM2 vers Spaces
# /etc/cron.daily/archive-logs.sh
#!/bin/bash
set -e
DATE=$(date +%Y-%m-%d)
TARBALL="/tmp/api-logs-$DATE.tar.gz"

tar czf "$TARBALL" /var/log/api/ /var/log/nginx/

# s3cmd configuré une fois avec SPACES_KEY / SPACES_SECRET
s3cmd put "$TARBALL" "s3://my-app-logs/$(date +%Y/%m)/" --acl-private

# Nettoyage local
rm "$TARBALL"
find /var/log/api/ -name '*.log.*' -mtime +7 -delete
Checklist mise en production :
  • Backups Droplet activés (--enable-backups au create)
  • Monitoring agent installé + alertes CPU/RAM/disque configurées
  • Endpoint /health qui teste DB, Redis, services tiers
  • Snapshots auto pré-déploiement dans le workflow (rollback < 5 min)
  • UFW activé : seuls 22, 80, 443 ouverts
  • SSH durci : PermitRootLogin no, PasswordAuthentication no
  • Required reviewers sur l'environnement production
  • Notifications Slack en cas d'échec ET de succès
  • Workflow de rollback testé une fois par trimestre
  • Logs archivés vers Spaces (rétention > 90 jours)

Conclusion

GitHub Actions et DigitalOcean forment un duo CI/CD redoutable pour les équipes à la recherche de simplicité et de coûts maîtrisés. Avec un Droplet à 6 $/mois, une clé SSH dédiée et un workflow YAML d'environ 80 lignes, vous obtenez un pipeline de déploiement digne d'une stack enterprise — tests automatisés, secrets chiffrés, rolling updates et rollback en un clic.

Pour aller plus loin, basculez sur l'architecture blue-green avec Floating IP dès que votre trafic dépasse quelques milliers de visiteurs/jour : le coût additionnel d'un second Droplet (6 $/mois) est négligeable face à la garantie de zero-downtime. Les utilisateurs qui préfèrent éviter la gestion serveur trouveront dans App Platform (5 $/mois) un compromis intéressant — moins de contrôle, mais aucune maintenance Linux à charge.

Quelle que soit la stratégie retenue, n'oubliez jamais le triptyque de base : monitoring activé, healthcheck robuste et rollback testé. Un bon pipeline CI/CD n'est pas celui qui déploie vite, mais celui qui revient en arrière sans paniquer le vendredi à 17h.

Récapitulatif — votre stack de déploiement DO + GitHub Actions :
  • Droplet Ubuntu 24.04 LTS (6 $/mois) avec backups auto
  • Utilisateur deploy sudoers + clé SSH dédiée GitHub
  • PM2 cluster mode + Nginx + certbot Let's Encrypt
  • Workflow GitHub Actions : test → build → rsync → reload PM2
  • Secrets : DO_SSH_KEY, DO_HOST, DO_API_TOKEN
  • Floating IP + 2e Droplet pour blue-green zero-downtime
  • doctl pour snapshots pré-deploy + bascule Floating IP
  • Spaces (5 $/mois 250 Go) pour assets et archives logs
  • DO Monitoring agent + Alert Policies vers Slack
  • Workflow rollback manuel avec required reviewers

Partager