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 |
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
--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'
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 }}"
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
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 }}
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
- 2 Droplets minimum (blue + green) ou App Platform 2 instances
- Floating IP attachée (gratuite si en usage)
- Endpoint
/healthqui 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 assignsur 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
/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
- Backups Droplet activés (
--enable-backupsau create) - Monitoring agent installé + alertes CPU/RAM/disque configurées
- Endpoint
/healthqui 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.
- Droplet Ubuntu 24.04 LTS (6 $/mois) avec backups auto
- Utilisateur
deploysudoers + 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