Vous avez une image testée, taguée, publiée sur un registre. Il lui manque la dernière marche : arriver sur un serveur et y tourner. C’est le moment où la chaîne devient un vrai déploiement continu — du commit jusqu’à la production, sans intervention manuelle, mais sous contrôle. Dans ce dernier tutoriel, vous connectez GitHub Actions à un VPS par SSH pour qu’il récupère et lance la nouvelle image de l’API Récolte, derrière une approbation humaine et avec un filet de sécurité pour revenir en arrière.
Ce tutoriel fait partie de la série « CI/CD avec GitHub Actions ». Pour la vue d’ensemble, lisez d’abord le guide principal.
🎯 Ce que vous allez apprendre
- Préparer un VPS pour recevoir des déploiements automatiques (Docker Compose, utilisateur dédié).
- Générer une clé SSH de déploiement et la stocker en secret, sans exposer votre clé personnelle.
- Écrire un workflow qui se connecte en SSH et met à jour le conteneur via
docker compose pull. - Protéger la production par l’environnement et l’approbation manuelle vus précédemment.
- Vérifier la santé du service après déploiement et savoir revenir en arrière (rollback).
🛠️ Ce que vous allez construire
On boucle la chaîne. L’image ghcr.io/votre-compte/recolte-api publiée au tutoriel précédent va être déployée sur un VPS : à chaque mise à jour de main (et après votre approbation), GitHub se connecte au serveur, tire la nouvelle image et redémarre le conteneur. Un contrôle de santé confirme que l’API répond, et vous saurez revenir à la version précédente en cas de pépin.
Prérequis
- Avoir suivi Construire et publier une image Docker : votre image est sur
ghcr.io. - Un VPS accessible en SSH, avec Docker et le plugin Compose installés (voir notre tutoriel déploiement Docker sur VPS).
- La maîtrise des secrets et environnements.
- ⏱️ Temps estimé : ~50 minutes.
Étape 1 — Préparer le VPS
Le serveur doit savoir quoi lancer avant que GitHub ne lui parle. On y dépose un docker-compose.yml qui référence l’image du registre — pas un build local, mais l’image publiée. C’est tout l’intérêt d’avoir poussé l’image : le serveur ne compile rien, il télécharge un artefact prêt à l’emploi.
# /opt/recolte/docker-compose.yml (sur le VPS)
services:
api:
image: ghcr.io/votre-compte/recolte-api:main
restart: unless-stopped
ports:
- "80:3000"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/sante"]
interval: 30s
timeout: 5s
retries: 3
Le champ image pointe vers le tag main de votre registre. restart: unless-stopped relance le conteneur après un redémarrage du serveur. Le healthcheck interroge une route /sante que l’API doit exposer (un simple 200 OK) : Docker saura ainsi si le conteneur est réellement opérationnel, pas seulement démarré. Si votre image est privée, le VPS doit d’abord s’authentifier au registre une fois avec docker login ghcr.io ; pour une image publique, le pull fonctionne sans connexion.
Un mot sur le compte qui recevra ces connexions. Évitez de déployer en root : créez un utilisateur dédié, par exemple deploy, et ajoutez-le au groupe docker pour qu’il puisse piloter les conteneurs sans être administrateur de la machine. C’est le même principe de moindre privilège que pour les permissions du GITHUB_TOKEN ou l’utilisateur non-root dans l’image : la session de CI ne dispose que de ce qu’il lui faut, déployer, et de rien d’autre. Si la clé venait à fuiter, le dégât possible reste circonscrit à la gestion des conteneurs, pas à tout le serveur.
Étape 2 — Une clé SSH dédiée au déploiement
GitHub doit pouvoir se connecter au VPS, mais hors de question d’y mettre votre clé personnelle : un secret de CI peut être lu par toute personne qui modifie le workflow. On crée donc une paire de clés dédiée, qu’on pourra révoquer sans impacter le reste. Sur votre machine :
ssh-keygen -t ed25519 -C "deploy-recolte" -f deploy_key
# génère deploy_key (privée) et deploy_key.pub (publique)
On installe la clé publique sur le VPS, dans le fichier des clés autorisées du compte de déploiement, et on garde la clé privée pour GitHub :
# sur le VPS, ajouter la clé publique
cat deploy_key.pub >> ~/.ssh/authorized_keys
Dans les secrets du dépôt (idéalement de l’environnement production), créez trois entrées : VPS_HOST (l’adresse du serveur), VPS_USER (le compte de déploiement) et VPS_SSH_KEY (le contenu intégral de deploy_key, la clé privée). Cette séparation est saine : la clé ne sert qu’au déploiement, vit chiffrée dans GitHub, et se révoque en une ligne sur le VPS si elle venait à fuiter — sans toucher à vos autres accès.
Étape 3 — Le workflow de déploiement (quick win)
Le cœur du tutoriel. On déclenche le déploiement sur les push vers main, on rattache le job à l’environnement production (donc à l’approbation manuelle configurée à l’étape secrets), et on utilise une action éprouvée, appleboy/ssh-action, pour exécuter des commandes à distance.
name: Déploiement
on:
push:
branches: [main]
jobs:
deployer:
runs-on: ubuntu-latest
environment: production # impose l'approbation manuelle
steps:
- name: Se connecter au VPS et mettre à jour
uses: appleboy/ssh-action@v1.2.5
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd /opt/recolte
docker compose pull
docker compose up -d
docker image prune -f
L’action ouvre une session SSH avec les trois secrets, puis exécute le bloc script sur le serveur, ligne par ligne. docker compose pull télécharge la nouvelle image depuis ghcr.io ; docker compose up -d recrée le conteneur avec cette image, en arrière-plan ; docker image prune -f nettoie les anciennes images pour ne pas saturer le disque. Comme le job cible l’environnement production, il se met en attente d’approbation : rien ne part en production sans votre clic. Poussez sur main, approuvez, et observez le serveur se mettre à jour tout seul.
Une précaution de sécurité mérite qu’on s’y arrête : par défaut, une première connexion SSH accepte aveuglément l’empreinte du serveur. Sur un poste, on vous demande « voulez-vous faire confiance à cet hôte ? » ; en CI, ce dialogue n’existe pas, ce qui ouvre théoriquement la porte à une attaque de l’homme du milieu si quelqu’un détournait l’adresse. La parade propre consiste à fournir l’empreinte attendue du serveur via une option fingerprint de l’action (qu’on récupère sur le VPS avec ssh-keyscan). Pour un projet personnel sur un VPS de confiance, le risque est faible ; pour de la vraie production, épingler l’empreinte est le réflexe à prendre — exactement dans l’esprit « ne rien accepter à l’aveugle » qui guide tout ce parcours.
✅ Point d’étape — Après approbation, le job doit se terminer en vert et votre API répondre avec la nouvelle version. Vérifiez sur le serveur avec
docker compose psque le conteneur tourne et est « healthy ».
Étape 4 — Vérifier la santé après déploiement
Déployer sans vérifier, c’est espérer. Un bon pipeline confirme que le service répond après la mise à jour, et signale l’échec sinon. On ajoute un step qui interroge la route de santé depuis le runner, une fois le conteneur relancé. S’il ne répond pas, le job échoue — et vous êtes prévenu immédiatement.
- name: Vérifier la santé du service
run: |
for i in 1 2 3 4 5; do
if curl -fsS https://api.recolte.exemple/sante; then
echo "Service en ligne ✅" && exit 0
fi
echo "Tentative $i échouée, nouvel essai dans 5s…"
sleep 5
done
echo "Le service ne répond pas après déploiement" && exit 1
On laisse au conteneur le temps de démarrer en réessayant cinq fois, espacées de cinq secondes. Le -f de curl fait échouer la commande sur une réponse d’erreur HTTP ; combiné au exit 1 final, il garantit que le job devient rouge si l’API reste muette. Ce contrôle transforme un déploiement « lancé » en déploiement « vérifié » : vous ne découvrez plus une panne par un message d’utilisateur mécontent, mais par une notification GitHub, dans la minute.
Étape 5 — Savoir revenir en arrière
Même testée, une version peut se comporter mal en production — une variable d’environnement oubliée, un cas limite réel. Le réflexe à acquérir n’est pas d’éviter l’erreur à tout prix, mais de pouvoir revenir vite à la version d’avant. C’est là que les tags d’image du tutoriel précédent paient. Plutôt que de toujours suivre main, on peut épingler une version précise et la changer en cas de souci.
# sur le VPS, revenir à une version connue comme stable
cd /opt/recolte
# éditer docker-compose.yml : image: ghcr.io/...:sha-9f2c1a8
docker compose pull && docker compose up -d
Comme chaque image porte un tag d’empreinte (sha-…) unique et immuable, revenir en arrière revient à pointer le docker-compose.yml vers le tag de la dernière version saine, puis à relancer. L’opération prend quelques secondes — le temps d’un pull d’une image déjà connue, donc en cache. Pour aller plus loin, on peut faire du rollback une action manuelle déclenchable depuis GitHub avec workflow_dispatch et un champ « tag à déployer », mais le principe reste celui-ci : une version stable est toujours à un up -d de distance.
Notez enfin une alternative pour un autre type de projet : si vous déployiez un site statique (une vitrine, une documentation) plutôt qu’une API, GitHub Actions sait publier directement sur GitHub Pages avec actions/upload-pages-artifact et actions/deploy-pages, sans aucun serveur à gérer. Pour notre API conteneurisée, le déploiement par SSH sur un VPS reste la voie naturelle.
✅ Point d’étape — Vous devez pouvoir, en une minute, repointer le serveur vers un tag d’empreinte antérieur et restaurer le service. Testez-le une fois à froid : c’est le genre de manœuvre qu’on ne veut pas découvrir en pleine panne.
🐞 Pièges fréquents
| Symptôme / erreur | Cause probable | Correctif |
|---|---|---|
Permission denied (publickey) |
Clé publique absente du VPS ou mauvais utilisateur | Vérifier authorized_keys et le secret VPS_USER |
Error response from daemon: pull access denied |
Image privée, VPS non connecté à ghcr.io | docker login ghcr.io sur le VPS, ou rendre l’image publique |
docker: command not found en SSH |
Docker absent du PATH du shell non interactif | Utiliser le chemin complet ou vérifier l’installation Docker |
| Le déploiement part sans approbation | Job non rattaché à l’environnement protégé | Ajouter environment: production au job |
| Le conteneur tourne mais l’API ne répond pas | Mauvais mappage de ports ou route de santé inexistante | Vérifier ports: et exposer une route /sante |
🌍 Adaptation au contexte ouest-africain
Le déploiement par SSH sur un VPS est, en pratique, la voie la plus accessible quand on n’a pas les moyens d’un cloud managé facturé à la fonction. Un VPS d’entrée de gamme chez un hébergeur international, ou une machine chez un fournisseur local, suffit : le pipeline ne demande qu’un accès SSH et Docker. On obtient un déploiement continu de qualité professionnelle pour le prix d’un petit serveur mensuel — sans verrou propriétaire.
L’approbation manuelle prend ici tout son sens. Dans une petite équipe, le responsable garde la main sur le moment du déploiement, y compris depuis son téléphone, ce qui évite les mises en production hasardeuses quand la connexion est mauvaise ou qu’on n’est pas devant son poste. Et la capacité de rollback en quelques secondes est un filet décisif là où une intervention physique sur le serveur serait lente ou impossible : revenir à la version stable se fait à distance, depuis n’importe où.
✅ Récapitulatif
La chaîne est complète. Vous avez préparé un VPS avec un docker-compose.yml qui consomme l’image du registre, généré une clé SSH dédiée et rangé proprement les secrets de connexion. Vous avez écrit un workflow qui, après votre approbation, se connecte au serveur, tire la nouvelle image et redémarre le service — puis vérifie qu’il répond vraiment. Et vous savez revenir en arrière en pointant un tag d’empreinte stable. Du git push au conteneur en production, chaque maillon est désormais automatisé, vérifié et réversible. C’est exactement ce qu’on attend d’un pipeline de déploiement continu.
🧾 Aide-mémoire
| Élément | Rôle |
|---|---|
image: ghcr.io/…:main |
Le serveur tire l’image, ne la construit pas |
ssh-keygen -t ed25519 |
Clé de déploiement dédiée |
appleboy/ssh-action@v1.2.5 |
Exécuter des commandes sur le VPS |
docker compose pull && up -d |
Mettre à jour le conteneur |
environment: production |
Approbation manuelle avant déploiement |
Route /sante + curl -f |
Vérifier que le service répond |
Tag sha-… |
Cible de rollback immuable |
💪 À vous de jouer
1. Ajoutez un workflow de rollback manuel (workflow_dispatch) qui prend en entrée un tag d’image et le déploie sur le VPS.
2. Faites précéder le déploiement d’un job de tests, pour que rien ne parte sans une suite verte.
Voir une piste pour le défi 1
on:
workflow_dispatch:
inputs:
tag:
description: "Tag d'image à déployer"
required: true
jobs:
rollback:
runs-on: ubuntu-latest
environment: production
steps:
- uses: appleboy/ssh-action@v1.2.5
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd /opt/recolte
IMAGE="ghcr.io/votre-compte/recolte-api:${{ github.event.inputs.tag }}"
docker compose pull && IMAGE_TAG="$IMAGE" docker compose up -d
Le champ inputs.tag apparaît dans l’interface au lancement manuel : vous y saisissez le tag d’empreinte stable, et le workflow le déploie. À combiner avec une variable d’environnement dans le docker-compose.yml pour piloter le tag proprement.
Tutoriels frères
- Construire et publier une image Docker — la source de l’image que ce déploiement consomme.
- Secrets et environnements — la clé SSH et l’approbation qui sécurisent ce déploiement.
Pour aller plus loin
- 🔝 Retour au guide principal : GitHub Actions : le guide CI/CD pour bien démarrer
- Documentation officielle : GitHub Docs — déploiements et appleboy/ssh-action.
- Voir aussi notre tutoriel déploiement Docker sur VPS pour préparer le serveur en détail.
FAQ
SSH ou OIDC pour déployer ?
Pour un VPS classique, SSH avec une clé dédiée est le plus simple et le plus répandu. L’OIDC brille face aux fournisseurs cloud (AWS, GCP, Azure) capables de valider une identité fédérée, comme vu au tutoriel sur les secrets.
Faut-il vraiment l’approbation manuelle ?
Pas obligatoire, mais fortement conseillée en début de parcours. Beaucoup d’équipes déploient automatiquement en préproduction et exigent une approbation seulement pour la production. Vous ajustez selon votre niveau de confiance.
Comment gérer les migrations de base de données ?
Ajoutez un step (ou une commande dans le script SSH) qui applique les migrations avant de relancer le conteneur. L’ordre compte : migrer la base, puis basculer l’application.
Le déploiement provoque-t-il une coupure ?
Avec un seul conteneur, up -d entraîne une brève interruption le temps du redémarrage. Pour du zéro-coupure, il faut un répartiteur de charge devant plusieurs instances — un sujet à part entière, au-delà de ce tutoriel.
Et si je n’ai pas de VPS ?
Pour un site statique, GitHub Pages déploie sans serveur via deploy-pages. Pour une API, un petit VPS reste le plus accessible ; certains hébergeurs proposent aussi des plateformes qui tirent directement une image de registre.
Mots-clés : déploiement continu GitHub Actions, déployer sur VPS SSH, appleboy ssh-action, docker compose pull, environnement production approbation, rollback, health check.