Déployer automatiquement depuis GitHub Actions vers un VPS
Pousser sur main et voir l'app se déployer toute seule sur le VPS. GitHub Actions le permet. Voici comment construire ce pipeline, du workflow au déploiement SSH sécurisé.
Déployer automatiquement une application sur son VPS à chaque push sur main réduit les manipulations manuelles et rend les mises en production reproductibles. Le développeur pousse son code sur GitHub, les tests s’exécutent, l’image Docker est construite, puis le VPS récupère la nouvelle version.
GitHub Actions est la solution CI/CD intégrée à GitHub. Les workflows s’exécutent généralement sur des runners hébergés par GitHub, sans serveur d’intégration à administrer. Ce guide prépare aussi l’arrivée prochaine des VPS TalCloud, hébergés en France avec stockage NVMe, protection Anti-DDoS Netrix, cadre RGPD et support humain.
Comprendre un workflow GitHub Actions
Un workflow est un fichier YAML placé dans le répertoire .github/workflows/ du dépôt. Il décrit quand et comment une automatisation doit s’exécuter.
Ses principaux composants sont les suivants :
ondéfinit les événements déclencheurs, comme un push surmainou l’ouverture d’une pull request.jobsregroupe les tâches indépendantes du workflow.stepscontient les commandes et actions exécutées dans chaque job.runs-onchoisit le runner utilisé, par exempleubuntu-latest.needsimpose un ordre entre plusieurs jobs.
Les runners hébergés par GitHub sont des machines temporaires préparées automatiquement. Ils conviennent bien aux builds, tests et déploiements classiques. Un runner self-hosted est installé sur une machine administrée par votre équipe. Il offre davantage de contrôle, mais augmente la surface d’attaque et les besoins de maintenance. Il est déconseillé d’installer directement un runner sur le VPS de production sans isolation stricte.
Les actions réutilisables du marketplace évitent de réécrire certaines opérations. actions/checkout récupère le dépôt, actions/setup-node installe Node.js et les actions Docker facilitent l’authentification et la construction d’images. Fixez leurs versions majeures et surveillez leurs mises à jour.
Construire et tester une application Node.js
Voici un premier workflow complet pour une application Node.js. Il s’exécute lors des push et des pull requests ciblant main.
name: Build and test
on:
push:
branches:
- main
pull_request:
branches:
- main
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build application
run: npm run build
npm ci installe exactement les versions enregistrées dans package-lock.json. Il est donc préférable à npm install dans un pipeline. Adaptez la version de Node.js à celle utilisée en production et ajoutez, si nécessaire, les commandes de lint ou de vérification des types.
Gérer les secrets sans les exposer
Une clé SSH, un mot de passe ou un jeton de registre ne doit jamais apparaître en clair dans le workflow. Stockez ces valeurs dans les GitHub Secrets du dépôt ou dans ceux d’un environnement GitHub.
Pour le déploiement, les secrets courants sont :
VPS_HOST: adresse IP ou nom DNS du VPS.VPS_USER: compte utilisé pour le déploiement.VPS_SSH_KEY: clé privée SSH dédiée.VPS_SSH_PORT: port SSH, généralement22.VPS_HOST_FINGERPRINT: empreinte de la clé hôte du serveur.
Une valeur est référencée avec ${{ secrets.VPS_HOST }}. GitHub masque les secrets reconnus dans les logs, mais cette protection ne justifie pas de les afficher volontairement. Évitez notamment set -x, les commandes echo et les scripts qui copient un secret dans un fichier publié comme artefact. Notre guide sur la gestion des secrets approfondit ces règles.
Les environnements GitHub permettent de séparer staging et production. Ils peuvent aussi exiger l’approbation d’un reviewer avant que le job accède aux secrets de production.
Stratégie 1 : publier une image dans GHCR
Une stratégie robuste consiste à construire une image Docker sur GitHub Actions, puis à la publier dans GitHub Container Registry, sous ghcr.io. Le VPS ne compile rien : il récupère exactement l’image validée par le pipeline.
Dans le workflow, GITHUB_TOKEN permet de s’authentifier auprès de GHCR. Le job doit recevoir la permission packages: write. docker/login-action ouvre la session et docker/build-push-action construit puis publie l’image, à partir d’un Dockerfile présent dans le dépôt.
Chaque image devrait recevoir un tag immuable, par exemple le SHA du commit. Un tag latest peut être ajouté pour simplifier l’exploitation, mais il ne suffit pas pour un rollback fiable.
Sur le VPS, Docker Compose référence ensuite l’image :
services:
app:
image: ghcr.io/talcloud-demo/my-app:${IMAGE_TAG:-latest}
restart: unless-stopped
ports:
- "3000:3000"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
Pour une image privée, le VPS doit être connecté à GHCR avec un jeton limité à la lecture des packages. Le GITHUB_TOKEN du workflow ne doit pas être copié sur le serveur.
Stratégie 2 : déclencher le déploiement par SSH
GitHub Actions peut ouvrir une connexion SSH vers le VPS après les tests. Deux approches sont possibles : utiliser appleboy/ssh-action ou lancer le client ssh natif depuis le runner.
La commande distante peut effectuer un git pull, mais cette méthode lie la production au dépôt, aux dépendances locales et aux outils de compilation. Avec Docker, il est généralement plus fiable d’exécuter :
cd /opt/my-app
docker compose pull
docker compose up -d --remove-orphans
La clé privée est stockée dans VPS_SSH_KEY. Sa clé publique correspondante est ajoutée au fichier authorized_keys du compte de déploiement. La vérification de l’identité du VPS reste indispensable. Avec une action SSH, utilisez son paramètre d’empreinte. Avec le client natif, préparez un fichier known_hosts à partir d’une empreinte vérifiée par un canal de confiance.
Workflow complet de test et déploiement
Ce workflow teste l’application à chaque pull request. Lors d’un push sur main, il publie aussi deux tags dans GHCR, puis déploie le tag correspondant au commit.
L’exemple suppose que le nom complet du dépôt GitHub est compatible avec un nom d’image GHCR en minuscules.
name: Test, publish and deploy
on:
push:
branches:
- main
pull_request:
branches:
- main
permissions:
contents: read
packages: write
concurrency:
group: production-deployment
cancel-in-progress: false
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build application
run: npm run build
publish:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs:
- test
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ github.sha }}
ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
needs:
- publish
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy through SSH
uses: appleboy/ssh-action@v1
env:
IMAGE_TAG: ${{ github.sha }}
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
port: ${{ secrets.VPS_SSH_PORT }}
fingerprint: ${{ secrets.VPS_HOST_FINGERPRINT }}
envs: IMAGE_TAG
script: |
set -eu
cd /opt/my-app
export IMAGE_TAG
docker compose pull
docker compose up -d --remove-orphans
docker compose ps
Le fichier compose.yaml du VPS doit utiliser la variable IMAGE_TAG, comme dans l’exemple précédent. Le cache GitHub Actions accélère les builds sans modifier l’image produite.
Préparer le VPS
Avant le premier déploiement, préparez le serveur manuellement :
- Créez un utilisateur
deploysans accès root direct. - Ajoutez sa clé publique dédiée dans
~deploy/.ssh/authorized_keys. - Installez Docker Engine et le plugin Docker Compose.
- Autorisez
deployà exécuter uniquement les opérations nécessaires. - Créez
/opt/my-appet placez-y le fichiercompose.yaml. - Connectez Docker à GHCR avec un jeton disposant seulement de
read:packages. - Vérifiez le pare-feu, les ports publiés et la protection SSH.
- Testez une première fois
docker compose pulletdocker compose up -d.
L’ajout au groupe docker donne des privilèges proches de root. Sur un serveur sensible, préférez Docker rootless ou une politique sudo limitée et soigneusement validée.
Sécuriser et fiabiliser le déploiement
La clé SSH de CI/CD doit être réservée au déploiement. Ne réutilisez ni une clé personnelle ni une clé donnant accès à plusieurs serveurs. Utilisez un compte non-root, désactivez l’authentification SSH par mot de passe et vérifiez systématiquement la clé hôte avec known_hosts ou une empreinte attendue.
Limitez le déploiement à main et protégez cette branche avec des pull requests et des checks obligatoires. Associez le job deploy à l’environnement production, puis activez des reviewers requis si votre offre GitHub le permet.
La section concurrency empêche deux mises en production simultanées. Pour revenir en arrière, conservez les tags basés sur les SHA : remplacez IMAGE_TAG par celui du dernier commit stable, puis relancez Docker Compose.
Enfin, ajoutez un healthcheck applicatif après docker compose up. Une simple commande HTTP avec plusieurs tentatives permet de détecter un conteneur démarré mais inutilisable. Pour un rollback automatique, conservez le tag précédent avant le déploiement et restaurez-le si le contrôle échoue.
Dépanner les erreurs fréquentes
Permission denied (publickey) indique généralement une mauvaise clé, un utilisateur incorrect, des permissions invalides sur .ssh ou une clé publique absente de authorized_keys. Une erreur de vérification de l’hôte signale plutôt une empreinte ou une entrée known_hosts incorrecte.
Une réponse unauthorized de GHCR vient souvent d’un jeton expiré, d’une permission packages insuffisante ou d’un package auquel le dépôt n’a pas accès. Vérifiez packages: write pour la publication et une autorisation de lecture séparée sur le VPS.
Si docker compose pull échoue uniquement sur le serveur, contrôlez la session avec docker login ghcr.io, le nom exact de l’image et le tag demandé. Consultez ensuite docker compose logs, l’état du healthcheck et l’espace disque disponible avant de relancer le déploiement.