Mettre en place un pipeline CI/CD avec GitLab CI
Compiler, tester et déployer à chaque push, automatiquement. GitLab CI l'intègre au dépôt. Voici comment construire un pipeline complet, du .gitlab-ci.yml au déploiement sur VPS.
Automatiser la compilation, les tests et le déploiement à chaque push réduit les erreurs humaines et accélère la livraison des applications. Avec GitLab CI/CD, cette automatisation est directement intégrée au dépôt GitLab ou GitLab.com.
La configuration repose principalement sur un fichier .gitlab-ci.yml. Il décrit les commandes à exécuter, leur ordre, les conditions de déclenchement et les fichiers à conserver. Une fois ce fichier poussé, GitLab crée automatiquement un pipeline.
Ce guide prolonge notre tutoriel pour héberger son propre GitLab, mais s’applique aussi à GitLab.com.
Comprendre les concepts de GitLab CI/CD
Un pipeline représente l’ensemble du processus d’intégration et de déploiement continus. Il est généralement déclenché par un push, une merge request, un tag ou une planification.
Le pipeline est divisé en stages, par exemple build, test et deploy. Les stages s’exécutent dans l’ordre déclaré. Les jobs d’un même stage peuvent fonctionner en parallèle si des runners sont disponibles.
Un job contient les commandes nécessaires à une tâche précise : installer les dépendances, lancer un linter, exécuter les tests ou déployer l’application. Chaque job est exécuté par un GitLab Runner.
Toute la configuration se trouve dans .gitlab-ci.yml, placé à la racine du dépôt. GitLab lit ce fichier à chaque modification et affiche le résultat du pipeline dans l’interface du projet.
Installer et configurer un GitLab Runner
Les jobs ne s’exécutent pas directement sur le serveur GitLab. Ils sont pris en charge par des GitLab Runners, des agents chargés de préparer l’environnement puis d’exécuter les commandes.
GitLab.com propose des runners partagés. Leur disponibilité et leurs limites dépendent de la configuration du compte et du projet. Pour une instance GitLab auto-hébergée, il faut généralement installer et enregistrer ses propres runners.
Un runner devrait idéalement fonctionner sur une machine séparée de GitLab et de la production. Cette isolation limite les conséquences d’un job défectueux ou compromis. L’executor Docker est un choix courant, car chaque job s’exécute dans un conteneur propre et reproductible.
Après l’installation de GitLab Runner, l’enregistrement s’effectue avec :
gitlab-runner register
La commande demande notamment l’URL de l’instance GitLab, un token d’authentification, une description et le type d’executor. Le token est fourni dans les paramètres CI/CD du projet, du groupe ou de l’instance. Pour Docker, il faut également sélectionner une image par défaut.
Créer un premier fichier .gitlab-ci.yml
Prenons une application Node.js avec des scripts lint, test et build déclarés dans son package.json. Ce premier pipeline installe les dépendances, vérifie le code, lance les tests et construit l’application.
stages:
- build
- test
default:
image: node:22-alpine
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
install_and_build:
stage: build
script:
- npm install
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
lint:
stage: test
script:
- npm install
- npm run lint
unit_tests:
stage: test
script:
- npm install
- npm test
Le mot-clé image définit l’image Docker utilisée par les jobs. script contient les commandes exécutées dans l’ordre. Si une commande retourne un code différent de zéro, le job échoue.
Pour une installation strictement reproductible, npm ci est souvent préférable à npm install. Dans ce cas, mettre en cache le répertoire de téléchargement de npm est généralement plus efficace :
cache:
key:
files:
- package-lock.json
paths:
- .npm/
before_script:
- npm ci --cache .npm --prefer-offline
Utiliser les artefacts et le cache
Les artefacts conservent les fichiers produits par un job. Ils permettent de transmettre un dossier dist/, un rapport de test ou un binaire aux jobs suivants. Ils peuvent aussi être téléchargés depuis l’interface GitLab.
Le cache répond à un autre besoin : accélérer les pipelines en réutilisant des fichiers entre plusieurs exécutions. Pour Node.js, il peut contenir node_modules/ ou le cache local de npm.
Un artefact représente donc un résultat du pipeline, tandis qu’un cache représente une optimisation. Un cache peut être supprimé ou indisponible : le pipeline doit toujours pouvoir réussir sans lui.
Protéger les variables et les secrets
Les identifiants, tokens et clés privées doivent être définis dans les paramètres CI/CD du projet ou du groupe. GitLab les injecte ensuite comme variables d’environnement dans les jobs.
Il ne faut jamais écrire un secret en clair dans .gitlab-ci.yml, même dans un dépôt privé. L’historique Git conserve les anciennes versions et rend la suppression complète difficile.
GitLab permet de marquer une variable comme masquée afin de limiter son affichage dans les logs. Une variable protégée est uniquement disponible pour les branches et tags protégés. Les secrets de production doivent être à la fois masqués, lorsque leur format le permet, et protégés. Notre guide sur la gestion des secrets complète ces bonnes pratiques.
Une clé SSH peut être enregistrée comme variable de type fichier. Le job reçoit alors le chemin d’un fichier temporaire plutôt que la valeur brute de la clé.
Construire et pousser une image Docker
GitLab intègre un registre de conteneurs. Les variables prédéfinies CI_REGISTRY, CI_REGISTRY_IMAGE, CI_REGISTRY_USER et CI_REGISTRY_PASSWORD permettent au pipeline de s’y authentifier sans identifiants écrits en dur.
Voici un pipeline complet qui teste une application, construit son image Docker, la pousse dans le registre puis prépare un déploiement :
stages:
- test
- build
- deploy
variables:
IMAGE_TAG: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
DOCKER_TLS_CERTDIR: "/certs"
node_tests:
stage: test
image: node:22-alpine
cache:
key:
files:
- package-lock.json
paths:
- .npm/
script:
- npm ci --cache .npm --prefer-offline
- npm run lint
- npm test
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
build_image:
stage: build
image: docker:27-cli
services:
- name: docker:27-dind
alias: docker
needs:
- job: node_tests
artifacts: true
before_script:
- printf '%s' "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" --username "$CI_REGISTRY_USER" --password-stdin
script:
- docker build --pull --tag "$IMAGE_TAG" .
- docker push "$IMAGE_TAG"
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
deploy_production:
stage: deploy
image: alpine:3.20
needs:
- build_image
environment:
name: production
before_script:
- apk add --no-cache openssh-client
- mkdir -p ~/.ssh
- install -m 600 "$SSH_PRIVATE_KEY" ~/.ssh/id_ed25519
- printf '%s\n' "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
script:
- printf '%s' "$CI_REGISTRY_PASSWORD" | ssh "$DEPLOY_USER@$DEPLOY_HOST" "docker login '$CI_REGISTRY' --username '$CI_REGISTRY_USER' --password-stdin"
- ssh "$DEPLOY_USER@$DEPLOY_HOST" "cd '$DEPLOY_PATH' && IMAGE_TAG='$IMAGE_TAG' docker compose pull && IMAGE_TAG='$IMAGE_TAG' docker compose up -d --remove-orphans"
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual
Ce pipeline utilise Docker-in-Docker pour construire l’image. Le runner doit autoriser ce fonctionnement, souvent avec un mode privilégié. Une autre approche consiste à exposer le socket Docker de l’hôte, mais elle donne au job un contrôle important sur la machine. Dans les deux cas, le runner doit être isolé et correctement sécurisé.
Déployer sur un VPS avec SSH
Le job de déploiement se connecte au VPS avec une clé privée stockée dans SSH_PRIVATE_KEY. Les variables DEPLOY_HOST, DEPLOY_USER, DEPLOY_PATH et SSH_KNOWN_HOSTS doivent également être configurées dans GitLab.
La clé publique correspondante doit être ajoutée au fichier authorized_keys du compte de déploiement sur le serveur. Ce compte doit disposer uniquement des permissions nécessaires. Il vaut mieux éviter une connexion SSH directe en tant que root.
Le fichier compose.yaml présent sur le VPS peut utiliser la variable transmise par le pipeline :
services:
app:
image: ${IMAGE_TAG}
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
La règle rules limite le déploiement à main, tandis que when: manual impose une validation humaine pour la production. Pour un déploiement automatique, cette ligne peut devenir when: on_success.
Cette architecture peut être utilisée sur un VPS TalCloud lorsque l’offre sera disponible. L’hébergement en France, la conformité RGPD, le stockage NVMe, la protection Anti-DDoS Netrix et le support humain répondent aux besoins fréquents des applications déployées par CI/CD.
Appliquer les bonnes pratiques
Un pipeline fiable doit appliquer quelques principes simples :
- Déployer uniquement depuis une branche protégée comme
main. - Utiliser des environnements GitLab distincts pour la recette et la production.
- Ajouter une validation manuelle avant la production.
- Protéger et masquer les variables sensibles.
- Empêcher les jobs non fiables d’accéder aux secrets de production.
- Isoler les runners des serveurs GitLab et des machines de production.
- Réserver les runners privilégiés aux projets qui en ont réellement besoin.
- Limiter les droits du compte SSH et du compte utilisé par Docker.
- Épingler les versions majeures des images utilisées par le pipeline.
- Conserver des artefacts et rapports de tests pendant une durée adaptée.
- Prévoir une stratégie de retour à la version précédente.
Il faut également contrôler les contributions externes. Une merge request peut modifier .gitlab-ci.yml et tenter d’afficher ou d’exfiltrer des variables. Les variables protégées et les runners réservés aux branches protégées réduisent ce risque.
Résoudre les erreurs courantes
Un job qui reste en attente indique souvent qu’aucun runner compatible n’est disponible. Vérifiez que le runner est actif, autorisé pour le projet et associé aux éventuels tags déclarés dans le job.
Un échec pendant docker build signale fréquemment un executor mal configuré, un service Docker-in-Docker inaccessible ou l’absence du mode privilégié. Consultez les logs du runner et vérifiez que le nom d’hôte docker est joignable depuis le job.
Une variable vide peut provenir d’un secret absent, d’un nom incorrect ou d’une variable protégée utilisée depuis une branche non protégée. Il faut comparer le nom exact de la variable et les règles de protection, sans afficher sa valeur dans les logs.
Enfin, un déploiement SSH qui échoue peut être lié aux droits de la clé, à une empreinte absente de known_hosts, au pare-feu ou aux permissions Docker du compte distant. Tester chaque étape séparément dans un job temporaire facilite le diagnostic, à condition de ne jamais afficher les secrets.