Avancé ⏱ 30 min de mise en œuvre Mis à jour le 11 août 2026

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 :

  • on définit les événements déclencheurs, comme un push sur main ou l’ouverture d’une pull request.
  • jobs regroupe les tâches indépendantes du workflow.
  • steps contient les commandes et actions exécutées dans chaque job.
  • runs-on choisit le runner utilisé, par exemple ubuntu-latest.
  • needs impose 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éralement 22.
  • 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 :

  1. Créez un utilisateur deploy sans accès root direct.
  2. Ajoutez sa clé publique dédiée dans ~deploy/.ssh/authorized_keys.
  3. Installez Docker Engine et le plugin Docker Compose.
  4. Autorisez deploy à exécuter uniquement les opérations nécessaires.
  5. Créez /opt/my-app et placez-y le fichier compose.yaml.
  6. Connectez Docker à GHCR avec un jeton disposant seulement de read:packages.
  7. Vérifiez le pare-feu, les ports publiés et la protection SSH.
  8. Testez une première fois docker compose pull et docker 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.