📍 Article principal du sujet : Incus 6 LTS — gérer conteneurs système et VMs Linux pour PME ouest-africaine
Pour une équipe qui adopte GitLab comme forge git, la question des runners CI/CD arrive vite : faut-il payer les minutes GitLab.com (10 USD/mois pour 10 000 minutes en plan Premium), ou auto-héberger des runners qui exécutent les pipelines sur votre infrastructure ? L’auto-hébergement gagne dès que la consommation dépasse quelques heures par mois — et avec Incus, on obtient en bonus une isolation forte par job grâce aux conteneurs éphémères : chaque exécution se passe dans une instance neuve, créée au démarrage, supprimée à la fin. Aucune contamination entre builds, aucune fuite de secrets entre projets, et un environnement reproductible à 100 %.
Ce tutoriel monte un runner GitLab CI/CD branché sur un GitLab.com gratuit ou un GitLab CE auto-hébergé, qui spawne dynamiquement un conteneur Incus par job. Sur un Hostinger Cloud VPS 4 Go RAM, on tient confortablement deux à quatre jobs concurrents — largement de quoi servir une équipe de cinq développeurs avec des pipelines build + tests + deploy.
Pourquoi des conteneurs éphémères Incus plutôt que Docker
L’option docker executor est le défaut sur la plupart des tutos GitLab Runner. Elle fonctionne, mais avec des limitations : les images Docker doivent être bâties pour chaque CI, le sandbox peut être contourné via mounts privilégiés, les ressources ne se partagent pas équitablement. Le custom executor avec Incus apporte trois bénéfices concrets :
- Isolation supérieure : chaque conteneur est une instance Linux complète avec son init, son réseau, son filesystem isolé via id-mapping. Une compromission dans un job reste cantonnée.
- Snapshots ZFS de base : on prépare un conteneur modèle avec tout ce qu’il faut (Node.js, Python, Go, outils de build), et chaque job clone ce modèle en une seconde via copy-on-write.
- Pas de Docker dans Docker : si vos pipelines doivent construire des images Docker, l’imbrication DinD est sale ; avec Incus, on active security.nesting: true et on construit normalement.
Prérequis
- VPS Linux 64 bits, 4 Go de RAM minimum, 60 Go SSD
- Incus 6 LTS opérationnel — voir Installer Incus avec Zabbly
- Compte GitLab.com gratuit OU instance GitLab CE / Gitea Actions auto-hébergée — voir Auto-héberger Gitea avec runner Actions pour l’alternative légère
- Accès admin sur la forge pour générer un token de runner
- Niveau attendu : confortable Linux, notion de pipeline CI/CD
- Temps estimé : 60 à 90 minutes pour le runner complet
Le scénario type : un VPS 4 Go RAM Hostinger pour le runner, séparé du serveur Gitea/GitLab si auto-hébergé (ou simplement pointé vers GitLab.com). Cette séparation runner/forge évite que les jobs gourmands ralentissent la forge, et facilite le scaling — quand l’équipe grossit, on ajoute un second VPS runner sans toucher à la forge.
Étape 1 — Préparer l’image conteneur modèle
L’idée maîtresse : on prépare une fois une image Incus avec tous les outils de build dont vos pipelines ont besoin (Node, Python, Go, Docker buildkit, kubectl, terraform, etc.), on la publie comme image locale, et chaque job clone cette image en une opération copy-on-write quasi-instantanée.
incus launch images:debian/12 ci-template
incus shell ci-template
Dans le conteneur, installation de la pile CI standard. Cette étape prend dix à quinze minutes mais n’est faite qu’une seule fois.
apt update && apt upgrade -y
apt install -y curl wget git unzip build-essential ca-certificates gnupg
# Node.js 20 LTS
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt install -y nodejs
# Python 3 + pip + uv
apt install -y python3 python3-pip python3-venv
pip install --break-system-packages uv
# Go 1.22
wget https://go.dev/dl/go1.22.5.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.22.5.linux-amd64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> /etc/profile
# Docker buildkit
apt install -y docker.io docker-buildx
exit
De retour sur l’hôte, on arrête le conteneur, on le publie comme image locale, et on le supprime — l’image cloneable est conservée dans le store d’Incus.
incus stop ci-template
incus publish ci-template --alias ci-template-debian12
incus delete ci-template
incus image list local:
Étape 2 — Installer GitLab Runner sur l’hôte
Le runner GitLab tourne directement sur l’hôte (pas dans un conteneur), parce qu’il doit interagir avec l’API Incus de l’hôte pour spawner les conteneurs jobs. Le binaire est officiel GitLab.
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | bash
apt install -y gitlab-runner
gitlab-runner --version
Étape 3 — Préparer les scripts du custom executor
Le mode custom de GitLab Runner attend trois scripts shell : prepare.sh (créer l’environnement), run.sh (exécuter une commande), cleanup.sh (nettoyer). On crée ces scripts dans /etc/gitlab-runner/incus/.
mkdir -p /etc/gitlab-runner/incus
cat <<'EOF' > /etc/gitlab-runner/incus/prepare.sh
#!/usr/bin/env bash
set -eo pipefail
INSTANCE_NAME="ci-${CUSTOM_ENV_CI_JOB_ID}"
incus launch local:ci-template-debian12 "$INSTANCE_NAME" --ephemeral
for i in {1..30}; do
IP=$(incus list "$INSTANCE_NAME" -c 4 --format csv | cut -d' ' -f1)
[[ -n "$IP" ]] && break
sleep 1
done
echo "$INSTANCE_NAME ready at $IP"
EOF
chmod +x /etc/gitlab-runner/incus/prepare.sh
Le drapeau --ephemeral indique à Incus que l’instance doit être supprimée automatiquement à son arrêt. Couplé à un cleanup.sh qui fait incus stop, on obtient un cycle propre : créer → exécuter → arrêter → suppression auto.
cat <<'EOF' > /etc/gitlab-runner/incus/run.sh
#!/usr/bin/env bash
set -eo pipefail
INSTANCE_NAME="ci-${CUSTOM_ENV_CI_JOB_ID}"
incus exec "$INSTANCE_NAME" -- bash -l "$1"
EOF
chmod +x /etc/gitlab-runner/incus/run.sh
cat <<'EOF' > /etc/gitlab-runner/incus/cleanup.sh
#!/usr/bin/env bash
INSTANCE_NAME="ci-${CUSTOM_ENV_CI_JOB_ID}"
incus stop "$INSTANCE_NAME" --force 2>/dev/null || true
EOF
chmod +x /etc/gitlab-runner/incus/cleanup.sh
Trois scripts simples, totalisant moins de vingt lignes. C’est tout ce qu’il faut pour que GitLab Runner pilote Incus.
Étape 4 — Enregistrer le runner
Côté GitLab (Settings → CI/CD → Runners), récupérer le token d’enregistrement. Puis sur l’hôte :
gitlab-runner register \
--non-interactive \
--url "https://gitlab.com/" \
--registration-token "TOKEN_ICI" \
--executor "custom" \
--description "incus-runner-vps-hostinger" \
--tag-list "incus,linux,vps" \
--custom-prepare-exec /etc/gitlab-runner/incus/prepare.sh \
--custom-run-exec /etc/gitlab-runner/incus/run.sh \
--custom-cleanup-exec /etc/gitlab-runner/incus/cleanup.sh
Le runner apparaît immédiatement dans l’UI GitLab avec le statut vert online. Les jobs taggés incus dans .gitlab-ci.yml seront acheminés vers ce runner.
Étape 5 — Premier pipeline
Côté projet GitLab, créez un fichier .gitlab-ci.yml minimaliste pour valider la chaîne complète :
stages:
- build
- test
build:
stage: build
tags: [incus]
script:
- echo "Construction depuis $(uname -a)"
- node --version
- python3 --version
- go version
test:
stage: test
tags: [incus]
script:
- echo "Test ID job: $CI_JOB_ID"
- sleep 5
- echo "Test OK"
Au push, GitLab planifie le pipeline, le runner spawne deux conteneurs éphémères (un par étape), exécute les scripts, et supprime les conteneurs. La durée totale typique pour ce pipeline minimal : 25 secondes — création de conteneur incluse.
Étape 6 — Concurrence et isolation
Par défaut, GitLab Runner exécute un job à la fois. Pour autoriser plusieurs jobs concurrents (un par cœur CPU disponible serait l’idéal), on édite /etc/gitlab-runner/config.toml :
concurrent = 4
[[runners]]
name = "incus-runner-vps-hostinger"
url = "https://gitlab.com/"
executor = "custom"
# ...
Sur un VPS 4 Go RAM avec ZFS, quatre conteneurs ci-template-debian12 simultanés (chacun 200-400 Mo) consomment 1,2 à 1,6 Go en pic, soit largement dans la marge. Le facteur limitant devient alors le CPU pour les jobs de compilation lourde.
Étape 7 — Cache et artifacts entre jobs
Les conteneurs étant éphémères, les node_modules ou .gradle téléchargés s’évanouissent à la fin du job. Pour ne pas re-télécharger à chaque run, on monte un volume persistant Incus en lecture-écriture dans le conteneur, et GitLab Runner gère le cache via ce volume.
incus storage volume create default ci-cache
# Modifier prepare.sh pour attacher ce volume :
incus config device add "$INSTANCE_NAME" cache disk \
source=ci-cache pool=default path=/var/cache/ci
Le répertoire /var/cache/ci dans le conteneur pointe vers le volume partagé. npm config set cache /var/cache/ci/npm et autres directives font que les dépendances s’accumulent et se réutilisent entre les builds. Gain typique : un build npm passe de 90 secondes (cache vide) à 8 secondes (cache plein).
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
| Runner cannot find image | L’image ci-template-debian12 n’existe pas sur l’hôte du runner | Vérifier incus image list local: ; rebuilder l’image |
| Job timeout sans logs | Conteneur démarré mais pas d’IP — DHCP incusbr0 bloqué | Vérifier incus list et ip a show incusbr0 sur l’hôte |
| Cache partagé inaccessible | Permissions du volume incorrectes pour l’idmap isolé | Désactiver security.idmap.isolated sur le profil ci-template ou ajuster les UIDs sur le volume |
| Builds Docker nécessaires | Conteneur sans capabilité nesting | Activer security.nesting: true et security.privileged: true en dernier recours |
| Saturation disque après quelques jours | Conteneurs éphémères mal nettoyés | cleanup.sh doit garantir incus stop --force ; ajouter un cron de purge |
Adaptation au contexte ouest-africain
Pour une équipe de cinq développeurs à Abidjan qui consomme 30 heures de CI/CD par mois sur GitLab.com, la facture passe à 28 USD/mois en plan Premium (incluant les minutes au-delà du quota gratuit). Avec un runner Incus auto-hébergé sur un VPS Hostinger 4 Go RAM à 5-6 USD/mois, l’économie est immédiate, et la capacité devient essentiellement illimitée — le runner traite autant de jobs que le VPS peut soutenir, sans facture variable.
Pour les pipelines qui font du déploiement vers des clients, l’auto-hébergement apporte aussi des bénéfices : les credentials de production restent sur votre infra, jamais sur GitLab.com, et les jobs de déploiement vers des serveurs africains partent du VPS lui-même.
Un argument qui revient peu mais qui compte : la fiabilité réseau. Un pipeline GitLab.com qui plante en milieu de job parce que le réseau a pataugé entre Dakar et le DC GitLab, c’est un quart d’heure perdu. Avec un runner local sur un VPS, la communication runner↔conteneur job se passe sur le bridge interne, sans aucun saut internet.
Tutoriels frères
- Auto-héberger Gitea ou Forgejo avec CI/CD intégré — l’alternative tout-en-un.
- Héberger 100 conteneurs Incus sur un seul VPS — la dimension capacité.
- Profils Incus avec security.nesting pour Docker dans CI — pour les pipelines complexes.
Pour aller plus loin
- 🔝 Retour à l’article principal sur Incus
- Documentation officielle Custom Executor
- Création d’instances Incus (doc officielle)
- Pour pratiquer : Hostinger Cloud VPS 4 Go RAM
FAQ
Quelle différence avec le mode docker executor ?
Le custom executor avec Incus offre une isolation supérieure (instance Linux complète au lieu d’un namespace partagé), un démarrage plus rapide grâce au copy-on-write ZFS, et la possibilité de builder des images Docker sans DinD sale.
Peut-on cibler un runner spécifique pour des jobs sensibles ?
Oui via les tags du runner. Configurer un runner avec --tag-list "production,deploy" garantit qu’il ne traite que les jobs taggés.
Compatible avec GitLab CE auto-hébergé ?
Oui, exactement la même configuration. Pointer --url sur votre instance GitLab CE.
Plusieurs forges depuis un seul runner ?
Oui, en enregistrant plusieurs [[runners]] dans config.toml, chacun avec son URL et son token.
Performances sur un VPS 4 Go ?
Quatre jobs concurrents en mode warm démarrent en moins de deux secondes chacun. Pour 50 commits/jour et trois étapes par pipeline, le runner traite la charge sans saturer.