Cloud & Déploiement

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

Questions fréquentes

Générez une paire SSH (ssh-keygen -t ed25519), ajoutez la clé publique dans ~/.ssh/authorized_keys du Droplet, puis stockez la clé privée dans Settings > Secrets > Actions sous le nom DO_SSH_KEY. Utilisez l'action webfactory/ssh-agent@v0.9.0 pour la charger durant le workflow.

Un Droplet 1 GB RAM coûte 6 $/mois, suffisant pour la plupart des APIs Node.js. App Platform démarre à 5 $/mois (managé, sans SSH), et Spaces (S3-compatible) coûte 5 $/mois pour 250 GB de stockage avec CDN inclus. GitHub Actions offre 2000 minutes gratuites/mois sur les repos privés.

Combinez PM2 cluster mode (pm2 reload évite les coupures) avec une Floating IP. Déployez sur un Droplet de staging, faites pivoter la Floating IP vers le nouveau Droplet via doctl, puis détruisez l'ancien. Cette stratégie blue-green garantit zéro requête perdue.

DigitalOcean propose une tarification fixe et prévisible (6 $/mois vs facture AWS variable), une UI simplifiée, des Droplets prêts en 55 secondes, et doctl plus accessible que aws-cli. Pour un projet 1-10 serveurs sans besoin avancé (Lambda, IAM granulaire), DO réduit la complexité opérationnelle.

Partager