Ce guide installe GARM v0.2.1 sur une VM Ubuntu 24.04 qui fait déjà tourner Gitea. À la fin, GARM sera actif en tant que service systemd, le provider LXD sera enregistré, et l'API répondra sur le port 9997.
GARM (GitHub/Gitea Actions Runner Manager) s'installe de la même façon que Gitea : des binaires compilés statiquement, téléchargés depuis les releases GitHub du projet, et un service systemd. Aucun conteneur n'est nécessaire pour faire tourner GARM lui-même.
Prérequis
Section intitulée « Prérequis »- VM Ubuntu 24.04 avec 4 Go de RAM, 30 Go de disque
- Gitea installé et accessible (voir Installer Gitea)
- Accès SSH avec
sudosur la VM curl,taretsha256sum(présents par défaut sur Ubuntu 24.04)
Étape 1, Télécharger et vérifier les binaires
Section intitulée « Étape 1, Télécharger et vérifier les binaires »GARM publie trois archives distinctes, chacune accompagnée d'un fichier .sha256 généré par le
projet. Le serveur, la CLI et le provider LXD sont versionnés séparément : garm et
garm-cli suivent le dépôt cloudbase/garm, alors que garm-provider-lxd vit dans son propre
dépôt et porte sa propre numérotation. Comparer les sommes de contrôle avant d'installer garantit
que l'archive reçue est bien celle publiée par l'éditeur.
Sur la VM :
GARM_VERSION="v0.2.1"LXD_PROVIDER_VERSION="v0.1.5"ARCH="linux-amd64" # linux-arm64 sur une VM ARM
cd /tmp
# 1. Serveur GARM + son empreintecurl -sSLO "https://github.com/cloudbase/garm/releases/download/${GARM_VERSION}/garm-${ARCH}.tgz"curl -sSLO "https://github.com/cloudbase/garm/releases/download/${GARM_VERSION}/garm-${ARCH}.tgz.sha256"
# 2. CLI GARM + son empreintecurl -sSLO "https://github.com/cloudbase/garm/releases/download/${GARM_VERSION}/garm-cli-${ARCH}.tgz"curl -sSLO "https://github.com/cloudbase/garm/releases/download/${GARM_VERSION}/garm-cli-${ARCH}.tgz.sha256"
# 3. Provider LXD + son empreinte (dépôt séparé)curl -sSLO "https://github.com/cloudbase/garm-provider-lxd/releases/download/${LXD_PROVIDER_VERSION}/garm-provider-lxd-${ARCH}.tgz"curl -sSLO "https://github.com/cloudbase/garm-provider-lxd/releases/download/${LXD_PROVIDER_VERSION}/garm-provider-lxd-${ARCH}.tgz.sha256"
# 4. Vérifier les trois archives d'un coupsha256sum --check \ "garm-${ARCH}.tgz.sha256" \ "garm-cli-${ARCH}.tgz.sha256" \ "garm-provider-lxd-${ARCH}.tgz.sha256"La sortie attendue est une ligne OK par archive :
garm-linux-amd64.tgz: OKgarm-cli-linux-amd64.tgz: OKgarm-provider-lxd-linux-amd64.tgz: OKSi l'une des lignes affiche FAILED, arrêtez-vous : supprimez les fichiers et recommencez le
téléchargement. Un FAILED signifie que le contenu reçu ne correspond pas à ce que le projet a
publié.
Étape 2, Installer les binaires
Section intitulée « Étape 2, Installer les binaires »Chaque archive contient un unique exécutable, sans arborescence ni fichier de configuration. Les
deux commandes destinées à l'opérateur (garm, garm-cli) vont dans /usr/local/bin, tandis que
le provider externe est isolé dans /opt/garm/providers.d : GARM le lance comme un
sous-processus, il n'a aucune raison d'être dans le PATH des utilisateurs.
cd /tmpARCH="linux-amd64"
# Extraire les trois exécutablestar -xzf "garm-${ARCH}.tgz"tar -xzf "garm-cli-${ARCH}.tgz"tar -xzf "garm-provider-lxd-${ARCH}.tgz"
# Installer le serveur et la CLIsudo install -o root -g root -m 755 /tmp/garm /usr/local/bin/garmsudo install -o root -g root -m 755 /tmp/garm-cli /usr/local/bin/garm-cli
# Installer le provider LXD dans un répertoire dédiésudo mkdir -p /opt/garm/providers.dsudo install -o root -g root -m 755 /tmp/garm-provider-lxd /opt/garm/providers.d/garm-provider-lxd
# Vérifiergarm --version# → v0.2.1
garm-cli version# → garm-cli: v0.2.1Étape 3, Installer LXD (snap)
Section intitulée « Étape 3, Installer LXD (snap) »LXD est le provider local qui va créer les conteneurs runners. Sur Ubuntu 24.04, il s'installe via snap.
Épinglez explicitement le canal LTS 5.21/stable plutôt que de laisser snap choisir : le
canal latest suit la branche 6.x, qui change de comportement plus souvent et n'est pas la cible
testée par le provider.
# Installer LXD depuis le canal LTSsudo snap install lxd --channel=5.21/stable
# Initialiser LXD avec les paramètres par défautsudo lxd init --auto
# Vérifier que le socket Unix est présenttest -S /var/snap/lxd/common/lxd/unix.socket && echo "LXD OK"
# Ajouter votre utilisateur au groupe lxd (pour utiliser lxc sans sudo)sudo usermod -aG lxd ${USER}newgrp lxd
# Vérifier la versionlxd --version# → 5.21.5 LTS (ou plus récent sur le canal 5.21)Étape 4, Créer l'utilisateur système GARM
Section intitulée « Étape 4, Créer l'utilisateur système GARM »GARM tourne en tant qu'utilisateur dédié, sans shell, membre du groupe lxd pour accéder au
socket LXD.
sudo useradd \ --system \ --no-create-home \ --shell /usr/sbin/nologin \ --groups lxd \ garm
# Vérifierid garm# → uid=997(garm) gid=997(garm) groups=997(garm),988(lxd)Étape 5, Créer les répertoires et la configuration
Section intitulée « Étape 5, Créer les répertoires et la configuration »GARM lit deux fichiers TOML au démarrage. Le premier, /etc/garm/config.toml, décrit le
serveur lui-même : écoute HTTP, authentification JWT, base de données, liste des providers. Le
second est propre au provider externe LXD et n'est jamais lu par GARM : le serveur se contente
de transmettre son chemin au sous-processus via la variable d'environnement
GARM_PROVIDER_CONFIG_FILE. Cette séparation explique pourquoi les deux fichiers n'ont ni la même
syntaxe ni les mêmes clés.
-
Créer les répertoires :
Fenêtre de terminal sudo mkdir -p /etc/garm /var/lib/garmsudo chown garm:garm /var/lib/garm && sudo chmod 750 /var/lib/garmsudo chown root:garm /etc/garm && sudo chmod 750 /etc/garm -
Générer les secrets, puis créer
/etc/garm/config.toml:Fenêtre de terminal # 32 caractères hexadécimaux exactement, tirés du générateur du systèmeGARM_DB_PASSPHRASE=$(openssl rand -hex 16)GARM_JWT_SECRET=$(openssl rand -base64 48)sudo tee /etc/garm/config.toml > /dev/null << EOF[default][jwt_auth]secret = "${GARM_JWT_SECRET}"time_to_live = "24h"[apiserver]bind = "0.0.0.0"port = 9997use_tls = false[logging]log_level = "info"log_format = "text"[database]backend = "sqlite3"passphrase = "${GARM_DB_PASSPHRASE}"[database.sqlite3]db_file = "/var/lib/garm/garm.db"[[provider]]name = "lxd_local"description = "LXD local"provider_type = "external"[provider.external]provider_executable = "/opt/garm/providers.d/garm-provider-lxd"config_file = "/etc/garm/garm-provider-lxd.toml"EOF -
Créer
/etc/garm/garm-provider-lxd.toml:Attention, ce fichier n'a pas de section
[lxd]: les clés du provider LXD sont à la racine du document TOML. Le provider refuse de démarrer avec un message trompeur,error validating config: unix_socket or address must be specified, si les clés sont imbriquées dans une table.Fenêtre de terminal sudo tee /etc/garm/garm-provider-lxd.toml > /dev/null << 'EOF'# Socket Unix local, prioritaire sur "url" (connexion réseau + TLS)unix_socket_path = "/var/snap/lxd/common/lxd/unix.socket"instance_type = "container"project_name = "default"include_default_profile = falsesecure_boot = false# Dépôt d'images Ubuntu officiel. Sans aucun dépôt déclaré, le provider# ne peut utiliser que les images déjà présentes sur le serveur LXD.[image_remotes][image_remotes.ubuntu]addr = "https://cloud-images.ubuntu.com/releases"public = trueprotocol = "simplestreams"skip_verify = falseEOF -
Fixer les permissions :
Fenêtre de terminal sudo chown garm:garm /etc/garm/config.toml /etc/garm/garm-provider-lxd.tomlsudo chmod 640 /etc/garm/config.toml /etc/garm/garm-provider-lxd.toml
Étape 6, Créer le service systemd
Section intitulée « Étape 6, Créer le service systemd »L'unité ci-dessous utilise Type=exec plutôt que Type=simple. La différence compte au moment
du diagnostic : avec Type=simple, systemd considère le service comme démarré dès qu'il a forké,
et systemctl start renvoie 0 même si le binaire est absent ou non exécutable. Type=exec
attend la réussite de l'appel execve(), ce qui fait remonter immédiatement une erreur
203/EXEC. Les directives ProtectSystem=strict et ReadWritePaths limitent par ailleurs
l'écriture au seul répertoire de données, pour qu'une compromission de GARM ne se traduise pas en
écriture sur le reste du système de fichiers.
sudo tee /etc/systemd/system/garm.service << 'EOF'[Unit]Description=GARM - GitHub/Gitea Actions Runner ManagerAfter=network.target
[Service]Type=execUser=garmGroup=garmWorkingDirectory=/var/lib/garmExecStart=/usr/local/bin/garm -config /etc/garm/config.tomlRestart=on-failureRestartSec=5sStandardOutput=journalStandardError=journalNoNewPrivileges=trueProtectSystem=strictReadWritePaths=/var/lib/garm /etc/garmReadOnlyPaths=/opt/garm
[Install]WantedBy=multi-user.targetEOF
sudo systemctl daemon-reloadsudo systemctl enable garmsudo systemctl start garmValidation
Section intitulée « Validation »Trois contrôles suffisent à confirmer que le serveur est opérationnel : le service est active,
le port 9997 est en écoute, et l'API répond. Le point qui surprend concerne le code HTTP :
tant que le compte administrateur n'existe pas, GARM renvoie 409 Conflict sur toutes les
routes de l'API, y compris /api/v1/. Ce n'est pas une erreur, c'est la façon dont le serveur
signale qu'il attend son initialisation. Le code passera à 401 Unauthorized une fois l'étape 7
terminée.
# Vérifier que le service tournesudo systemctl status garm --no-pager
# Vérifier que le socket est en écoutess -tlnp | grep 9997# → LISTEN 0 4096 0.0.0.0:9997 0.0.0.0:* users:(("garm",pid=...,fd=16))
# Tester l'API avant initialisation (409 = GARM répond, il attend garm-cli init)curl -s -o /dev/null -w "%{http_code}\n" http://localhost:9997/api/v1/# → 409Étape 7, Initialiser le compte admin
Section intitulée « Étape 7, Initialiser le compte admin »GARM ne crée pas de compte admin automatiquement. La première commande garm-cli init crée
l'administrateur et configure le profil local de garm-cli. Cette opération est à faire une
seule fois sur chaque installation.
read -r -s -p "Mot de passe admin GARM : " GARM_ADMIN_PASSWORD; echo
garm-cli init \ --name "mon-garm" \ --url "http://localhost:9997" \ --username "garmadmin" \ --password "${GARM_ADMIN_PASSWORD}" \ --email "admin@garm.lab"La commande affiche deux tableaux, l'un décrivant l'utilisateur créé, l'autre le contrôleur. Retenez le Controller Webhook URL : c'est l'adresse que Gitea devra appeler lors de l'intégration.
Vérification :
# Afficher les infos du contrôleurgarm-cli controller show# → Controller ID, Hostname, Metadata/Callback/Webhook URL, Version v0.2.1
# Lister les providersgarm-cli provider list# → NAME: lxd_local | DESCRIPTION: LXD local | TYPE: externalDépannage courant
Section intitulée « Dépannage courant »Le premier réflexe devant un service failed est de lire le journal plutôt que le statut : GARM
écrit la raison exacte du refus sur une seule ligne avant de s'arrêter, et cette ligne
identifie la section fautive du TOML. Les deux erreurs les plus fréquentes sur une première
installation portent sur la longueur de la passphrase et sur l'accès au socket LXD, qui dépend de
l'appartenance de l'utilisateur garm au groupe lxd.
| Symptôme | Cause | Solution |
|---|---|---|
passphrase must be…32 characters | Passphrase trop courte dans [database] | Régénérer avec openssl rand -hex 16, qui donne exactement 32 caractères |
unix_socket or address must be specified | Clés du provider imbriquées dans une table [lxd] | Mettre les clés à la racine de garm-provider-lxd.toml et utiliser unix_socket_path |
permission denied sur le socket LXD | L'utilisateur garm n'est pas dans le groupe lxd | sudo usermod -aG lxd garm puis sudo systemctl restart garm |
| Port 9997 déjà utilisé | Autre processus en écoute | ss -tlnp | grep 9997 pour identifier le processus |
garm-cli: token expired | Le token JWT a dépassé son time_to_live | garm-cli profile login -u garmadmin -p '<mot-de-passe>' |
Service en failed au démarrage | Erreur de configuration | journalctl -u garm -n 20 --no-pager pour lire la ligne d'erreur |
Sécurité, ce qu'il reste à durcir
Section intitulée « Sécurité, ce qu'il reste à durcir »Cette installation est fonctionnelle mais volontairement minimale : l'API écoute sur 0.0.0.0
en HTTP sans TLS. Sur une VM de lab isolée, c'est acceptable ; dès que la machine est
joignable au-delà de votre réseau d'administration, trois points sont à traiter.
- Restreindre l'écoute : passer
bind = "127.0.0.1"dans[apiserver]et exposer GARM derrière un reverse proxy qui termine le TLS. - Activer TLS nativement si vous ne voulez pas de proxy :
use_tls = trueplus une section[apiserver.tls]aveccertificateetkey. - Protéger les secrets sur disque :
/etc/garm/config.tomlcontient la passphrase de la base et le secret JWT en clair. Le mode640avecroot:garmde l'étape 5 est le minimum ; une sauvegarde de ce fichier doit être chiffrée au même titre que la base.
À retenir
Section intitulée « À retenir »- Les binaires
garm,garm-clietgarm-provider-lxdse téléchargent depuis les releases GitHub et se vérifient avec le fichier.sha256publié à côté de chaque archive. - Le provider LXD est versionné indépendamment du serveur : v0.1.5 pour GARM v0.2.1.
- La passphrase du bloc
[database]doit faire exactement 32 caractères (AES-256), d'oùopenssl rand -hex 16. - Le fichier du provider LXD n'a pas de section
[lxd]: les clés sont à la racine, et le socket se déclare avecunix_socket_path. - L'utilisateur
garmdoit être dans le groupelxdpour accéder au socket Unix de LXD. garm-cli initest une opération unique : elle crée l'admin et configure le profil local.- Avant initialisation l'API répond 409, après initialisation elle répond 401 : dans les deux cas, GARM tourne.