SSD Nodes Learn Hosting plans →
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-10-21

Installer un runner GitHub Actions sur un VPS

Installez un runner GitHub Actions auto-hébergé sur Ubuntu 24.04 avec utilisateur dédié, checksum, config.sh et service systemd. Voyez le risque des PR de forks.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

Ce que fait un runner GitHub Actions auto-hébergé

Un runner GitHub Actions auto-hébergé est un programme que vous installez sur votre propre VPS. Il demande des jobs à GitHub et les exécute sur votre matériel. Vous l’enregistrez auprès d’un dépôt, vous l’installez comme service systemd, puis il redémarre après chaque reboot. GitHub planifie le job. Votre serveur l’exécute.

La CI (intégration continue) sur une machine que vous contrôlez présente deux avantages. Les minutes de compilation ne sont plus décomptées. Un job peut aussi accéder à des ressources disponibles uniquement sur votre machine, comme un cache de compilation déjà alimenté ou un réseau privé. Le coût est la sécurité. Le runner exécute tout ce que le fichier de workflow indique, avec les droits de l’utilisateur que vous lui avez attribué. Un fichier de workflow permet donc par conception l’exécution de code à distance. Dans un dépôt privé, ce fonctionnement est acceptable, car seules les personnes auxquelles vous faites confiance peuvent en ajouter un. Dans un dépôt public, le risque est réel. La section consacrée aux pull requests provenant de forks explique le mécanisme.

Tout ce qui suit concerne Ubuntu 24.04 avec la version 2.338.0 du runner, qui était la version publiée la plus récente au 12 October 2026.

Ce dont vous avez besoin avant de commencer

Partez d’un VPS avec un compte d’administration standard et sudo, dans l’état décrit dans les dix premières minutes sur un nouveau VPS. Vous n’avez pas besoin d’ouvrir de port entrant. Le runner ouvre une connexion HTTPS (hypertext transfer protocol secure) sortante vers GitHub et la maintient ouverte en attendant du travail. GitHub ne se connecte donc jamais à votre serveur. Votre pare-feu peut rester fermé au trafic externe et les jobs continuent d’arriver.

Vous devez également disposer des droits d’administration sur le dépôt, car le registration token est affiché dans les paramètres du dépôt.

Créer un utilisateur dédié pour le runner

N’exécutez jamais le runner avec root ou avec votre propre compte d’administration. Chaque job hérite des droits de l’utilisateur du runner. Un workflow qui appelle sudo réussit donc si l’utilisateur du runner peut utiliser sudo. Créez un utilisateur non privilégié qui ne possède rien, à l’exception de son propre répertoire personnel. La section Comptes utilisateur avec le principe du moindre privilège sur un VPS présente le principe général. Voici la configuration adaptée à ce cas.

sudo useradd -m -s /bin/bash gharunner
sudo passwd -l gharunner
sudo chmod 750 /home/gharunner
sudo install -d -m 700 -o gharunner -g gharunner /home/gharunner/actions-runner

passwd -l verrouille le mot de passe. Personne ne peut donc se connecter en tant que gharunner avec ce mot de passe. Le mode 700 sur le répertoire du runner est important, car le runner y stocke ses identifiants en clair et un checkout peut contenir du code source privé.

Vérifiez ces deux propriétés avant de continuer :

sudo passwd -S gharunner
sudo -l -U gharunner

passwd -S affiche une ligne qui commence par gharunner L. L signifie que le mot de passe est verrouillé. sudo -l -U gharunner doit répondre avec is not allowed to run sudo. S’il affiche plutôt une liste de commandes autorisées, le compte appartient à un groupe sudo et l’isolation que vous venez de mettre en place n’existe plus.

Télécharger le runner et vérifier l’archive tar

À partir de maintenant, utilisez le compte runner.

sudo -iu gharunner
cd ~/actions-runner
RUNNER_VERSION=2.338.0
curl -fL -o actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz \
  "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz"

Exécutez d’abord uname -m si vous n’êtes pas certain de l’architecture. x86_64 utilise le fichier linux-x64 ci-dessus. aarch64 utilise actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz.

Vérifiez maintenant le fichier téléchargé. La valeur SHA256 (secure hash algorithm, 256 bits) ci-dessous correspond à l’archive tar x64 2.338.0. GitHub affiche la valeur de la release actuelle sur la page de la release et sur l’écran New self-hosted runner. Elle change à chaque version. Copiez-la depuis l’une de ces pages lorsque vous installez une autre version.

echo "af4b794c1bc41d73d40535e3fe092a39f9679cd8d965954c2aca25a05ca41d32  actions-runner-linux-x64-2.338.0.tar.gz" | sha256sum -c

Un téléchargement correct affiche une ligne :

actions-runner-linux-x64-2.338.0.tar.gz: OK

Un fichier tronqué ou modifié affiche l’échec et un avertissement :

actions-runner-linux-x64-2.338.0.tar.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match

Ne passez pas cette vérification et ne laissez pas tar détecter le problème à votre place. Une archive partiellement écrite échoue avec gzip: stdin: unexpected end of file et tar: Unexpected EOF in archive. Cela indique que le fichier est incorrect, mais pas s’il a été tronqué ou remplacé.

tar xzf ./actions-runner-linux-x64-2.338.0.tar.gz
ls

Contenu de l’archive tar et éléments absents

Après l’extraction, le répertoire contient config.sh, run.sh, env.sh, safe_sleep.sh, bin/ et externals/. bin/ contient les binaires du runner et bin/installdependencies.sh. externals/ contient le runtime Node fourni avec le bundle, que les actions JavaScript utilisent.

Il n’y a pas encore de svc.sh. La documentation GitHub le décrit comme le script « créé après l’ajout réussi du runner », car il est généré à partir d’un modèle qui intègre votre dépôt et le nom du runner dans le nom du service. sudo ./svc.sh install avant ./config.sh échoue donc avec sudo: ./svc.sh: command not found. Enregistrez d’abord le runner, puis installez le service.

Installer les dépendances du runner

Le runner est une application .NET. Il a donc besoin de quelques bibliothèques partagées. Restez dans le shell de l’utilisateur du runner et installez-les avec sudo, car le script écrit dans la base de données des paquets système.

exit
cd /home/gharunner/actions-runner
sudo ./bin/installdependencies.sh

Sur Ubuntu 24.04, cela installe libkrb5-3, zlib1g, liblttng-ust1t64, libssl3t64 et libicu74. Pour chaque bibliothèque, le script essaie plusieurs noms de version et conserve celui fourni par votre version. C’est pourquoi le même script fonctionne sur les anciennes versions d’Ubuntu et sur Debian.

Si vous ignorez cette étape et qu’une de ces bibliothèques est absente, ./config.sh s’arrête avant toute autre opération :

Dependencies is missing for Dotnet Core 6.0
Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies.

L’absence de libicu produit le même conseil, avec une première ligne différente : Libicu's dependencies is missing for Dotnet Core 6.0. Les deux messages proviennent du même mécanisme : avant de démarrer, config.sh vérifie les bibliothèques incluses avec ldd et libicu avec ldconfig. Une liaison non résolue arrête donc le script au lieu de provoquer un plantage difficile à diagnostiquer plus tard.

Enregistrer le runner auprès de votre dépôt

Récupérez un token dans le dépôt. Ouvrez Settings, puis Actions, puis Runners, puis New self-hosted runner. La page affiche un token d’enregistrement. Il expire une heure après sa création. Générez-le lorsque vous êtes prêt à le coller.

Effectuez l’enregistrement avec l’utilisateur du runner. config.sh refuse de s’exécuter avec sudo.

sudo -iu gharunner
cd ~/actions-runner
./config.sh --url https://github.com/YOUR-USER/YOUR-REPO \
  --token PASTE_REGISTRATION_TOKEN_HERE \
  --name vps-runner-1 \
  --labels vps \
  --work _work \
  --unattended \
  --replace

Rôle de ces options. --name définit le nom sous lequel le runner apparaît dans le dépôt. Choisissez donc un nom que vous reconnaîtrez encore dans six mois. --labels ajoute vos propres labels. Le runner possède déjà self-hosted, Linux et X64 sans intervention de votre part. --work définit le répertoire dans lequel les dépôts sont clonés, à l’intérieur du répertoire du runner. --unattended répond aux invites interactives avec leurs valeurs par défaut. C’est le comportement attendu lorsque la commande se trouve dans un script. --replace reprend une inscription existante portant le même nom au lieu d’échouer. C’est le comportement attendu lorsque vous reconstruisez le serveur.

Une exécution réussie se termine par ces lignes :

√ Runner successfully added
√ Runner connection is good
√ Settings Saved.

L’enregistrement se trouve maintenant dans le répertoire du runner, sous la forme de .runner, .credentials et .credentials_rsaparams. Les deux derniers fichiers identifient ce runner auprès de GitHub. Toute personne pouvant les lire peut donc usurper son identité. C’est pourquoi le répertoire utilise le mode 700 et que l’utilisateur ne dispose pas de sudo.

Installer le runner comme service systemd

./run.sh dans un terminal convient pour un seul test, mais le processus s’arrête avec votre session SSH. Installez le service pour que le runner démarre au boot. Services et timers systemd sur un VPS explique les fichiers d’unité eux-mêmes. Ici, svc.sh en crée un pour vous.

exit
cd /home/gharunner/actions-runner
sudo ./svc.sh install gharunner
sudo ./svc.sh start
sudo ./svc.sh status

svc.sh nécessite les droits root, car il écrit une unité dans /etc/systemd/system et l’active. L’argument qui suit install correspond à l’utilisateur sous lequel le service s’exécute. Indiquez explicitement gharunner. Sans argument, le script utilise $SUDO_USER, qui est votre compte d’administration. Chaque job s’exécute alors avec un utilisateur qui peut utiliser sudo.

L’unité porte le nom du dépôt et du runner, sous la forme actions.runner.YOUR-USER-YOUR-REPO.vps-runner-1.service. Vous n’avez jamais besoin de le saisir manuellement :

systemctl list-units 'actions.runner.*'
sudo journalctl -u 'actions.runner.*' -n 20 --no-pager

Un runner opérationnel journalise √ Connected to GitHub, puis une ligne qui se termine par Listening for Jobs. La page Runners du dépôt l’affiche alors comme Idle. Un runner affiché comme Offline n’est pas démarré ou ne peut pas joindre GitHub sur le port 443.

Envoyer un job au runner

runs-on sélectionne un runner selon son label. Demandez self-hosted ainsi que votre propre label, afin qu’un job ne soit pas exécuté sur un runner qui n’était pas prévu.

name: build
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: [self-hosted, linux, vps]
    steps:
      - uses: actions/checkout@v5
      - run: uname -a

Si le job reste en attente à Waiting for a runner to pick up this job, les labels ne correspondent pas. Chaque label de runs-on doit exister sur le runner. Un seul mot en trop laisse le job en file d’attente, sans erreur visible. Comparez la liste avec les labels affichés à côté du runner dans les paramètres du repository.

Pourquoi les runners self-hosted et les dépôts publics ne font pas bon ménage

C’est la partie que beaucoup de personnes ignorent. Les recommandations de GitHub sont claires : les runners self-hosted « ne devraient presque jamais être utilisés pour des dépôts publics » et ils « n’offrent aucune garantie d’exécuter les workflows dans des machines virtuelles temporaires et propres. Du code non fiable exécuté dans un workflow peut les compromettre de manière persistante ».

Le mécanisme est simple. Une pull request provenant d’un fork contient sa propre copie du fichier de workflow. Si votre dépôt public exécute les workflows de pull request sur votre runner, toute personne pouvant forker le dépôt peut proposer un workflow qui exécute ses commandes sur votre VPS. Aucun accès en écriture n’est nécessaire, car l’élément proposé est précisément celui qui sera exécuté.

Les paramètres d’approbation réduisent ce risque sans le supprimer. Par défaut, un dépôt public demande à un mainteneur d’approuver le workflow d’un fork soumis par un contributeur qui intervient pour la première fois. GitHub considère une personne comme contributeur initial jusqu’à ce qu’un de ses commits ou pull requests ait été fusionné dans le dépôt. Ensuite, ses pull requests suivantes s’exécutent sans demande d’approbation. GitHub avertit lui-même qu’une simple correction fusionnée suffit pour atteindre cette situation. Le contrôle repose donc sur la lecture humaine d’un diff, et une charge utile dissimulée trois niveaux plus bas dans un script de build peut facilement passer inaperçue.

Une pull request provenant d’un fork ne reçoit pas vos secrets, et son GITHUB_TOKEN est en lecture seule. Cela limite les dégâts dans GitHub. Cela ne protège pas votre serveur. L’attaquant dispose d’un shell sous l’identité de gharunner. Il peut donc lire tous les fichiers accessibles à cet utilisateur, atteindre tout ce que le VPS peut atteindre sur son réseau privé et laisser une porte dérobée dans ~/.bashrc ou dans une unité systemd utilisateur exécutée lors du job suivant.

L’enregistrement avec --ephemeral fait accepter un seul job au runner, puis le désenregistre. Un job ne peut donc pas lire le workspace du job suivant. Cela n’est utile que si la machine ou le conteneur est recréé pour chaque job, car une porte dérobée écrite dans le répertoire personnel de l’utilisateur du runner survit à un nouvel enregistrement.

Les règles qui suivent sont courtes. Utilisez des runners self-hosted pour les dépôts privés. Si vous devez en rattacher un à un dépôt public, n’y exécutez pas les pull requests provenant de forks, ne placez rien d’autre sur ce serveur et considérez la machine comme temporaire.

Les tâches Docker et le groupe qui équivaut réellement à root

Les tâches exécutées dans des conteneurs, les conteneurs de service et toute étape de workflow qui appelle docker build nécessitent un daemon Docker sur l’hôte du runner. Installez Docker de la manière habituelle, comme l’explique Docker et Docker Compose sur un VPS, puis ajoutez l’utilisateur du runner au groupe docker.

Comprenez le compromis avant de procéder. L’appartenance au groupe docker équivaut à root, car un conteneur peut monter / avec bind mount et s’exécuter en tant que root à l’intérieur. Un workflow qui peut communiquer avec le socket Docker peut donc lire et modifier tous les fichiers du VPS, y compris /etc/shadow. Dans un dépôt privé avec des contributeurs de confiance, ce compromis peut être acceptable. Ailleurs, il annule l’intérêt d’un utilisateur sans privilèges. Docker rootless maintient les builds de conteneurs dans les droits propres à l’utilisateur du runner, au prix d’un storage driver plus lent et de l’absence de conteneurs privilégiés.

Mises à jour et suppression propre du runner

Un runner self-hosted se met à jour par défaut. Il détecte une nouvelle release, remplace ses propres fichiers et redémarre le service. En général, vous n’avez donc rien à faire. Pour désactiver l’auto-update, transmettez --disableupdate à ./config.sh lors de l’enregistrement du runner. Pour un runner déjà enregistré, vous devez l’enregistrer de nouveau avec ce flag. Cette option ne permet pas de rester définitivement sur une seule version. La documentation de GitHub précise qu’un runner configuré avec --disableupdate doit tout de même être mis à jour manuellement dans les 30 jours suivant une nouvelle release. Sinon, GitHub cesse de lui attribuer des jobs.

Une mise à jour manuelle conserve l’enregistrement, car .runner et .credentials ne se trouvent pas dans l’archive tar. Arrêtez le service, téléchargez la nouvelle archive tar et vérifiez son checksum avec gharunner, extrayez-la dans le même répertoire avec tar xzf, puis redémarrez le service :

cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh start

Pour supprimer le runner, désinstallez d’abord le service, puis supprimez son enregistrement. Le removal token se trouve sur la même page Runners, sous le bouton Remove du runner concerné.

cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh uninstall
sudo -iu gharunner
cd ~/actions-runner
./config.sh remove --token PASTE_REMOVAL_TOKEN_HERE

Supprimer le répertoire sans supprimer l’enregistrement laisse le runner affiché comme Offline dans le repository. Il y reste jusqu’à ce qu’un administrateur clique sur Force remove this runner, ou jusqu’à ce que GitHub le supprime automatiquement après 14 jours sans connexion.

Modes d’échec et messages affichés

Must not run with sudo. config.sh affiche ce message et quitte lorsqu’il est exécuté en tant que root. Ce contrôle est volontaire, car les fichiers appartenant à root dans _work empêchent tous les jobs suivants exécutés avec le compte de service de fonctionner. Exécutez ./config.sh avec gharunner. La variable RUNNER_ALLOW_RUNASROOT permet de contourner ce contrôle, mais cela ne fait que reporter le problème.

sudo: ./svc.sh: command not found. Vous êtes dans le bon répertoire. svc.sh n’existe pas encore, car config.sh n’a pas terminé l’enregistrement d’un runner. Enregistrez le runner, puis installez le service.

Http response code: NotFound from 'POST https://api.github.com/actions/runner-registration'. En général, le token n’est pas un token d’enregistrement valide. Il a peut-être expiré, car sa durée de validité est limitée à une heure, ou bien un personal access token a été collé à la place du token d’enregistrement de la page Runners. Générez un nouveau token, collez-le à nouveau et vérifiez que --url contient l’URL complète du dépôt.

Dependencies is missing for Dotnet Core 6.0. Exécutez sudo ./bin/installdependencies.sh depuis le répertoire du runner avec root, puis recommencez l’enregistrement.

Runner hors ligne après un redémarrage. Exécutez systemctl is-enabled 'actions.runner.*'. Si rien ne s’affiche, ./svc.sh install n’a jamais été exécuté : le runner n’a donc existé que dans votre session de terminal. Si l’unité est activée et que le runner est toujours hors ligne, consultez journalctl -u 'actions.runner.*' et vérifiez les connexions HTTPS sortantes.

Le disque est plein. Les checkouts, les caches de build et les images Docker s’accumulent dans _work et dans le répertoire personnel de l’utilisateur du runner. Rien ne les supprime automatiquement. Surveillez du -sh /home/gharunner/actions-runner/_work et ajoutez un nettoyage planifié avant que le disque ne se remplisse.

FAQ

Pourquoi sudo ./svc.sh install affiche-t-il « command not found » ?

Parce que svc.sh ne se trouve pas dans l’archive tarball du runner. Il est généré dans le répertoire du runner lorsque ./config.sh termine l’enregistrement, en utilisant le nom de votre dépôt et celui du runner pour construire le nom du service. Exécutez d’abord ./config.sh avec l’utilisateur du runner. Ensuite, sudo ./svc.sh install gharunner trouve le script et écrit une unité nommée actions.runner.OWNER-REPO.RUNNER-NAME.service dans /etc/systemd/system.

Dois-je ouvrir un port du firewall pour un runner auto-hébergé ?

Non. Le runner ouvre une connexion HTTPS sortante vers GitHub et la maintient ouverte pendant qu’il attend des jobs. GitHub n’initie donc jamais de connexion vers votre VPS. Autorisez les connexions sortantes sur le port 443 et laissez vos règles entrantes fermées. Si le runner apparaît Offline alors que son service fonctionne, vérifiez le filtrage sortant et le DNS plutôt que les règles entrantes.

Puis-je utiliser un runner auto-hébergé sur un dépôt public ?

Oui, mais GitHub le déconseille. Une pull request provenant d’un fork contient son propre fichier de workflow. Toute personne pouvant forker votre dépôt peut donc proposer des commandes exécutées sur votre machine. La demande d’approbation par défaut concerne uniquement les contributeurs dont aucun commit ni aucune pull request n’a encore été fusionné dans le dépôt. Si vous rattachez un runner à un dépôt public, désactivez les workflows de pull request provenant de forks sur ce runner, n’utilisez ce serveur pour rien d’autre et reconstruisez la machine régulièrement.

Pourquoi l’enregistrement échoue-t-il avec Http response code: NotFound ?

L’appel d’enregistrement renvoie NotFound lorsque l’identifiant d’authentification est incorrect, et pas uniquement lorsque l’URL est incorrecte. Le message est donc trompeur. Les registration tokens expirent une heure après leur affichage, et un personal access token n’est pas accepté pour cet appel. Ouvrez de nouveau Settings, Actions, Runners, New self-hosted runner, copiez le nouveau token et vérifiez que la valeur --url pointe vers un dépôt sur lequel vous disposez des droits d’administration.

Cet article vous a-t-il été utile ?

#github-actions#ci#self-hosted#runner#ubuntu-24-04