GitHub Actions pour les pipelines de déploiement auto-hébergés
GitHub Actions est bien documenté pour les déploiements vers des plateformes PaaS cloud — Vercel, Railway, Fly.io, et autres environnements gérés similaires disposent tous d'intégrations natives. Mais en pratique, une part significative des infrastructures de production tourne sur des instances VPS auto-hébergées ou des serveurs bare-metal où le modèle de déploiement est fondamentalement différent. Il n'existe pas d'API push-to-deploy. Vous devez atteindre le serveur, copier les fichiers et exécuter des commandes. Cet article couvre les modèles que nous utilisons pour déployer des applications Docker Compose vers un serveur Hetzner auto-hébergé via GitHub Actions, notamment la gestion des secrets et les stratégies de rollback.
Contexte de l'infrastructure
Notre serveur de production tourne sous Debian sur un Hetzner CX53. Docker Compose gère la pile de services, et Traefik fait office de reverse proxy et de couche de terminaison TLS. Les déploiements impliquent la construction d'une image Docker, son push vers un registre, son pull sur le serveur, et le redémarrage du service concerné. Le frontend web est un site statique déployé via rsync plutôt que Docker, qui a un chemin de déploiement plus simple mais le même modèle d'accès.
Les runners GitHub Actions sont hébergés par GitHub (nous n'utilisons pas de runners auto-hébergés). Ils se connectent à notre serveur via SSH en utilisant une clé de déploiement. C'est un modèle standard mais avec des implications de sécurité : la clé de déploiement doit avoir accès au serveur, ce qui en fait un credential de haute valeur qui nécessite une gestion soigneuse à la fois dans GitHub Secrets et sur le serveur lui-même.
Configuration du déploiement SSH
La clé de déploiement est une paire de clés Ed25519 générée spécifiquement pour le CI. La clé privée est stockée comme GitHub Secret ; la clé publique est ajoutée à ~/.ssh/authorized_keys sur le serveur pour l'utilisateur de déploiement. Nous utilisons un utilisateur système dédié (deploy) avec un accès shell limité et la propriété uniquement sur les répertoires dans lesquels le processus de déploiement a besoin d'écrire. Cet utilisateur ne peut pas faire sudo et ne peut pas accéder aux répertoires personnels d'autres utilisateurs.
La connexion via SSH dans un workflow GitHub Actions nécessite de charger la clé privée dans l'agent SSH avant l'exécution de toute commande distante. Le modèle standard utilise ssh-agent et ssh-add dans les étapes du workflow, ou une action communautaire comme webfactory/ssh-agent qui gère le chargement des clés et la configuration des hôtes connus. Nous ajoutons la clé hôte du serveur aux hôtes connus du workflow au moment de la configuration pour éviter que les invites de vérification d'hôte interactive ne bloquent le pipeline.
Pour les déploiements rsync — notre site statique, par exemple — le workflow construit la sortie, puis exécute rsync -avz --delete via SSH pour synchroniser le répertoire dist/ vers le serveur. L'indicateur --delete garantit que les fichiers supprimés à la source sont supprimés de la destination, ce qui est important pour les sites statiques où les fichiers périmés peuvent provoquer des comportements inattendus. La chaîne de connexion utilise l'utilisateur de déploiement et un port SSH non standard si applicable.
Exécution distante Docker Compose
Le déploiement d'un service Docker Compose nécessite une approche différente. Le workflow doit : construire la nouvelle image (ou tirer une image pré-construite depuis un registre), puis sur le serveur distant tirer la nouvelle image et redémarrer le service avec un minimum d'interruption.
Nous utilisons un modèle d'exécution distante en deux étapes. La première commande SSH gère le pull : ssh deploy@server "docker pull registry/image:tag". La seconde gère le redémarrage : ssh deploy@server "docker compose -f /opt/stack/docker-compose.yml up -d --no-deps service_name". L'indicateur --no-deps empêche Docker Compose de redémarrer inutilement les services dépendants. Exécuter pull et redémarrage comme commandes séparées signifie qu'un échec du pull ne laisse pas le service dans un état partiellement mis à jour.
Pour les services qui nécessitent des migrations de base de données avant le démarrage de la nouvelle version, nous ajoutons une troisième commande SSH qui exécute la migration à l'intérieur de la nouvelle image de conteneur avant le redémarrage du service : docker run --rm --env-file /opt/stack/.env registry/image:tag migrate. Les migrations s'exécutent sur la base de données actuelle avant que le trafic ne bascule vers le nouveau conteneur. Cela suppose que les migrations sont rétrocompatibles — une exigence qui mérite sa propre discussion mais qui est un prérequis pour les déploiements sans interruption quelle que soit votre outillage de déploiement.
Nous passons le tag d'image comme entrée de workflow ou le dérivons du SHA du commit git. Tagger les images avec le SHA du commit plutôt que “latest” fournit un enregistrement non ambigu de ce qui tourne en production et simplifie le rollback — vous pouvez déployer n'importe quel tag précédent sans avoir à raisonner sur ce qu'était “latest” à un moment donné.
Gestion des secrets
GitHub Secrets stocke les credentials dont le workflow a besoin à l'exécution : clés privées SSH, credentials de registre, valeurs de variables d'environnement. GitHub masque ces valeurs dans les journaux de workflow, ce qui empêche une exposition accidentelle dans la sortie de build. Les secrets sont accessibles comme variables d'environnement dans les étapes de workflow : ${{ secrets.SSH_PRIVATE_KEY }}.
Les secrets d'application — les valeurs qui vont dans le fichier .env sur le serveur — sont une préoccupation distincte. Nous ne stockons pas les secrets d'application dans GitHub Secrets et ne les injectons pas au moment du déploiement. Au lieu de cela, le fichier .env vit sur le serveur et est géré indépendamment du pipeline de déploiement. Le déploiement ne met pas à jour ni ne remplace le fichier env ; il ne met à jour que le code en cours d'exécution. Cela signifie que les modifications des secrets d'application nécessitent une étape manuelle séparée sur le serveur, ce qui crée une porte intentionnelle plutôt que de faire des modifications de variables d'environnement une partie de chaque déploiement.
L'alternative — stocker tous les secrets d'application dans GitHub et les injecter lors du déploiement — est plus simple à raisonner mais concentre l'exposition des credentials. Si votre compte ou dépôt GitHub est compromis, un attaquant capable de déclencher une exécution de workflow aurait accès à tous les secrets d'application. Garder les secrets sur le serveur signifie qu'un attaquant a besoin à la fois d'un accès GitHub et d'un accès serveur pour les extraire.
Stratégies de rollback
La stratégie de rollback la plus simple pour un déploiement basé sur Docker est le redéploiement du tag d'image précédent. Comme nous tagguons les images avec les SHAs des commits git, le rollback signifie réexécuter le workflow de déploiement avec le SHA du commit précédent comme tag d'image. Cela peut être fait en revenant sur la branche git ou en déclenchant manuellement un dispatch de workflow avec le tag cible comme paramètre d'entrée.
Pour les déploiements rsync — le site statique — le rollback est un redéploiement de l'artefact de build précédent. Nous conservons les artefacts de build comme artefacts de workflow GitHub Actions pendant 30 jours. Pour un incident de production, nous téléchargeons l'artefact précédent et le synchronisons manuellement. C'est suffisamment rare pour qu'un processus manuel soit acceptable ; l'automatiser ajouterait une complexité de workflow qui n'est pas justifiée par la fréquence des rollbacks.
Un mode de défaillance courant dans les pipelines de déploiement auto-hébergés est un déploiement partiel qui laisse le service dans un état incohérent. L'approche Docker Compose gère cela bien car les redémarrages de conteneurs sont atomiques du point de vue du service — soit le nouveau conteneur démarre avec succès, soit l'ancien continue de tourner. Le scénario plus dangereux est une migration qui s'exécute avec succès sur une base de données avant qu'un redémarrage de conteneur échoue. À ce stade, le rollback du code peut ne pas être sûr si la migration a modifié le schéma d'une manière que la version précédente ne peut pas gérer. Nous abordons cela en exigeant que toutes les migrations soient rétrocompatibles et en exécutant un test de fumée contre le nouveau conteneur avant l'étape de redémarrage final du service dans le workflow.
Observabilité dans le pipeline
GitHub Actions fournit une journalisation intégrée pour chaque exécution de workflow, mais les journaux sont éphémères — ils sont supprimés après une période de conservation et ne se substituent pas à l'observabilité au niveau de l'application. Nous traitons les journaux de workflow comme des informations de diagnostic pour les échecs CI et nous appuyons sur la journalisation côté serveur (journaux JSON structurés envoyés à un agrégateur de journaux) pour l'investigation des incidents de production.
Un ajout qui s'est avéré payant : une étape finale dans chaque workflow de déploiement qui exécute un contrôle de santé contre le service déployé. Une simple requête HTTP vers le point de terminaison de santé du service avec un délai d'attente et une assertion non-200 déclenche un échec du workflow si le service ne s'est pas démarré correctement. Combiné avec une intégration d'alertes sur les échecs de workflow, cela donne un signal quasi-temps-réel qu'un déploiement a laissé le service dans un état cassé, avant que les clients ne le rencontrent.
Ressources connexes
Articles connexes
Guide complet de dépannage n8n auto-hébergé 2025 : résoudre les problèmes de taille des données d'exécution et de webhooks avec Traefik
Reprise après sinistre pour les services auto-hébergés : notre stratégie de sauvegarde
Analytique respectueuse de la vie privée : Configurer Plausible auto-hébergé avec Google Search Console