SSD Nodes Learn 8GB de RAM — $66/an
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-08-02

Installer un runner GitHub Actions sur un VPS

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

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

Fonctionnement d’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 en tant que 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 possédez présente deux avantages. Les minutes de build ne sont plus décomptées, et un job peut accéder à des ressources disponibles uniquement sur votre machine, comme un cache de build déjà alimenté ou un réseau privé. Le coût est la sécurité. Le runner exécute tout ce que définit le fichier de workflow, 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. Sur un dépôt privé, cela ne pose pas de problème, car seules les personnes auxquelles vous faites confiance peuvent en ajouter un. Sur 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 utilise Ubuntu 24.04 et la version 2.336.0 du runner, qui est la version actuelle en juillet 2026.

Ce dont vous avez besoin avant de commencer

Partez d’un VPS avec un compte d’administration standard et sudo, dans l’état obtenu après 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 monde extérieur et les jobs arrivent quand même.

Vous devez également disposer des droits d’administration sur le dépôt, car le token d’enregistrement 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 le compte root ou avec votre propre compte d’administration. Chaque tâche 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 sans privilèges qui ne possède rien à part son propre répertoire personnel. Comptes utilisateur appliquant le principe du moindre privilège sur un VPS présente le principe général. Voici la configuration spécifique.

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 avec ce mot de passe en tant que gharunner. 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 indique 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.336.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 uname -m en premier si vous n’êtes pas sûr 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 (algorithme de hachage sécurisé, 256 bits) ci-dessous correspond à l’archive tar x64 2.336.0. GitHub affiche la valeur de la version actuelle sur la page de la release et sur l’écran New self-hosted runner. Cette valeur change à chaque version. Copiez-la depuis ces emplacements si vous installez une autre version.

echo "04cf0be1aff4c3ec3554466c39124ca250e3effd8873bb7e8d68535aa9505d5d  actions-runner-linux-x64-2.336.0.tar.gz" | sha256sum -c

Un téléchargement réussi affiche une ligne :

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

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

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

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

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

Ce que contient l’archive tar et ce qu’elle ne contient pas

Après 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 inclus, 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 le nom de votre dépôt et celui 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 plusieurs bibliothèques partagées. Conservez le shell de l’utilisateur du runner et installez-les avec sudo, car le script modifie la base de données des paquets système.

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

Sous Ubuntu 24.04, cette commande installe libkrb5-3, zlib1g, liblttng-ust1t64, libssl3t64 et libicu74. Le script essaie plusieurs noms de version pour chaque bibliothèque et conserve celui fourni par votre release. Le même script fonctionne donc sur les anciennes versions d’Ubuntu et sur Debian.

Si vous ignorez cette étape, ./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 affiche le même conseil avec une première ligne différente, Libicu's dependencies is missing for Dotnet Core 6.0. Les deux messages ont la même origine : config.sh exécute ldd sur les bibliothèques incluses avant de démarrer. Un lien non résolu arrête donc le script au lieu de provoquer plus tard un crash difficile à diagnostiquer.

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

Obtenez un token auprès du dépôt. Ouvrez Settings, puis Actions, Runners et enfin New self-hosted runner. La page affiche un registration token qui commence par A. Il expire une heure après sa création. Générez-le donc 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

Voici le rôle de ces flags. --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 configuration supplémentaire. --work définit le répertoire dans lequel les checkouts sont placé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 souhaité lorsque la commande est exécutée dans un script. --replace reprend une registration existante portant le même nom au lieu d’échouer. C’est le comportement souhaité lorsque vous reconstruisez le serveur.

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

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

La registration est maintenant enregistrée dans le répertoire du runner sous les noms .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 a le mode 700 et que l’utilisateur n’a pas de droits 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 démarrage du système. services et timers systemd sur un VPS explique les fichiers 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 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 compte pouvant utiliser sudo.

L’unit est nommée d’après le dépôt et le runner, au format actions.runner.YOUR-USER-YOUR-REPO.vps-runner-1.service. Vous n’avez jamais besoin de saisir ce nom 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 se terminant par Listening for Jobs. La page Runners du dépôt l’affiche alors avec l’état 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 par label. Demandez self-hosted avec votre propre label afin qu’un job ne puisse pas être exécuté sur un runner non 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 mot supplémentaire laisse donc 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 dépôt.

Pourquoi les runners auto-hébergés et les dépôts publics sont incompatibles

C’est la partie que beaucoup ignorent. Les recommandations de GitHub sont claires : les runners auto-hébergés « ne devraient presque jamais être utilisés pour des dépôts publics » et ils « n’offrent aucune garantie d’exécution 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 capable de forker le dépôt peut proposer un workflow qui exécute ses commandes sur votre VPS. Elle n’a pas besoin d’un accès en écriture, car ce qu’elle propose est précisément ce qui sera exécuté.

Les paramètres d’approbation réduisent le risque sans le supprimer. Par défaut, pour un dépôt public, GitHub demande à un mainteneur d’approuver le workflow provenant du fork d’un contributeur qui intervient pour la première fois. Après votre approbation initiale, les pull requests suivantes de cette personne s’exécutent sans nouvelle demande. Le contrôle repose donc sur un humain qui examine un diff à chaque fois. Une charge utile dissimulée trois niveaux plus bas dans un script de build est facile à manquer.

Une pull request provenant d’un fork ne reçoit pas vos secrets, et son GITHUB_TOKEN est en lecture seule. Cela limite les dommages à l’intérieur de GitHub. Cela ne protège pas votre serveur. L’attaquant dispose d’un shell avec 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 un élément persistant 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. Ainsi, un job ne peut 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 backdoor é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 auto-hébergés pour les dépôts privés. Si vous devez en associer un à un dépôt public, n’y exécutez pas les pull requests provenant de forks, ne faites rien d’autre sur ce serveur et considérez la machine comme jetable.

Tâches Docker et groupe équivalent à root

Les tâches de conteneur, 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 indiqué dans Docker et Docker Compose sur un VPS, puis ajoutez l'utilisateur du runner au groupe docker.

Comprenez les conséquences avant de le faire. 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. Sur un dépôt privé avec des contributeurs de confiance, ce compromis peut être acceptable. Dans les autres cas, il annule l'intérêt d'utiliser un utilisateur sans privilèges. Docker rootless limite les builds de conteneurs aux droits de l'utilisateur du runner, au prix d'un driver de stockage plus lent et de l'absence de conteneurs privilégiés.

Mises à jour et suppression propre du runner

Un runner auto-hébergé se met à jour par défaut. Il détecte une nouvelle version, remplace ses propres fichiers et redémarre le service. En général, vous n’avez donc rien à faire. ./config.sh --disableupdate désactive la mise à jour automatique lorsque vous devez conserver une version donnée. Ensuite, vous devez effectuer les mises à jour vous-même : la documentation GitHub précise qu’un runner configuré avec --disableupdate doit être mis à jour manuellement.

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 et vérifiez la somme de contrôle de la nouvelle archive tar 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 désenregistrez le runner. Le removal token se trouve sur la même page Runners, dans 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 désenregistrer le runner le laisse affiché avec l’état Offline dans le repository. GitHub apprend uniquement qu’il a disparu lorsque le runner le signale ou qu’un administrateur supprime manuellement l’entrée.

Modes d’échec, avec les chaînes que vous verrez

Must not run with sudo. config.sh affiche ce message et se termine lorsqu’il est exécuté en tant que root. Ce contrôle est volontaire, car les fichiers appartenant à root dans _work interrompent toutes les tâches suivantes exécutées avec le compte de service. Exécutez ./config.sh en tant que gharunner. La variable RUNNER_ALLOW_RUNASROOT désactive ce contrôle, mais son utilisation ne fait que reporter le problème.

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

Http response code: NotFound from 'POST https://api.github.com/actions/runner-registration'. Le token n’est pas un token d’enregistrement valide. Il a soit expiré, car sa durée de validité est d’une heure, soit un personal access token a été collé à la place du token d’enregistrement de la page Runners. Générez un nouveau token, puis collez-le à nouveau.

Dependencies is missing for Dotnet Core 6.0. Exécutez sudo ./bin/installdependencies.sh depuis le répertoire du runner en tant que root, puis effectuez à nouveau l’enregistrement.

Runner hors ligne après un redémarrage. Exécutez systemctl is-enabled 'actions.runner.*'. Si aucun élément n’est listé, ./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 le trafic HTTPS sortant.

Le disque se remplit. 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. Aucun nettoyage automatique n’est effectué. Surveillez du -sh /home/gharunner/actions-runner/_work et ajoutez un nettoyage planifié avant que le disque ne soit saturé.

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 repository 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 self-hosted ?

Non. Le runner ouvre une connexion HTTPS sortante vers GitHub et la maintient ouverte en attendant les 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 affiche Offline alors que son service est en cours d’exécution, examinez le filtrage sortant et le DNS plutôt que les règles entrantes.

Puis-je utiliser un runner self-hosted sur un repository public ?

C’est possible, mais GitHub le déconseille. Une pull request provenant d’un fork contient son propre fichier de workflow. Toute personne pouvant forker votre repository peut donc proposer des commandes qui s’exécuteront sur votre machine. L’invite d’approbation ne concerne que la première exécution d’un contributeur. Si vous associez un runner à un repository public, désactivez les workflows des pull requests provenant de forks, ne conservez rien d’autre sur ce serveur 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 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 à nouveau Settings, Actions, Runners, New self-hosted runner, copiez le nouveau token et vérifiez que la valeur --url pointe vers un repository sur lequel vous disposez des droits d’administration.

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