Vos tests de régression visuelle passent sur votre portable. Vous poussez, le pipeline s'exécute, et les mêmes tests font planter le navigateur en silence ou signalent cinquante « changements » qui ne sont que des différences d'anticrénelage. Rien n'a changé dans votre code, mais la CI n'est pas d'accord.
Cet écart entre « ça marche en local » et « ça marche en CI » est presque toujours un problème d'infrastructure, pas un problème de test. Pour auto-héberger de façon fiable des tests de régression visuelle dans un pipeline CI, il faut quatre pièces correctement assemblées : un runner CI avec un exécuteur Docker, un conteneur configuré pour que Chromium headless ne manque jamais de mémoire partagée, un outil de comparaison réglé pour produire des rapports lisibles par la CI, et un VPS dimensionné pour la mémoire que les navigateurs headless consomment réellement.
Ce guide construit cette pile de bout en bout. À la fin, vous aurez un .gitlab-ci.yml fonctionnel (ou son équivalent Gitea), un environnement Docker qui empêche Chromium de planter, BackstopJS produisant des rapports JUnit que votre pipeline sait lire, et un VPS correctement dimensionné pour faire tourner le tout.
En bref
- Exécutez le navigateur dans la même image Docker en local et en CI. La plupart des faux positifs viennent des différences de polices et de rendu entre environnements. Épinglez l'image et ils disparaissent.
- Corrigez la valeur /dev/shm de 64MB par défaut. La taille de mémoire partagée par défaut de Docker affame le moteur de rendu de Chromium, qui plante silencieusement en CI. Définissez shm_size: '2gb' dans docker-compose.yml, ou passez --disable-dev-shm-usage à Chromium.
- Utilisez BackstopJS comme outil auto-hébergeable principal. Il est sous licence MIT (v6.3.25), pensé pour Docker via un flag --docker, et produit des rapports JUnit. Si vous utilisez déjà Playwright, son toHaveScreenshot() intégré est un très bon point de départ sans rien installer.
- Dimensionnez sur 4GB de RAM comme base. Prévoyez large côté mémoire. En conditions réelles, un job de capture Chromium peut consommer bien plus de mémoire pendant les captures pleine page qu'il n'y paraît au repos : 4GB de RAM constituent donc la base la plus sûre pour un à deux jobs séquentiels ou faiblement parallélisés.
Ce que ce guide ne couvre pas
C'est un guide de mise en place pour une infrastructure que vous avez déjà décidé de monter. Quelques points sont volontairement hors périmètre :
- Ce n'est pas un comparatif complet d'outils. Ce guide se concentre sur la pile que vous pouvez réellement exploiter dans un environnement CI auto-hébergé : BackstopJS, Docker et votre propre runner.
- Il ne couvre pas l'auto-hébergement d'Argos. Argos est open source, mais son parcours produit public et sa documentation portent sur l'application Argos hébergée et les intégrations CI plutôt que sur un chemin d'auto-hébergement simple en production. Pour l'objectif de ce guide, BackstopJS est l'exemple le plus sûr.
- Il ne recommande pas Lost Pixel. Ce projet a été archivé en avril 2026 ; utilisez BackstopJS pour tout nouveau déploiement.
- Il ne rejoue pas le débat auto-hébergement contre SaaS managé. Si vous êtes ici, vous avez déjà écarté Percy et Chromatic.
Prérequis
Il vous faut quelques éléments en place avant la première commande :
- Un runner CI auto-hébergé, ou un plan pour en déployer un (voir plus bas).
- Docker installé sur l'hôte du runner.
- Un projet Node.js avec une cible de test existante (l'URL d'une application en cours d'exécution ou un ensemble de routes de composants à capturer).
- Un accès shell au VPS ou à l'hôte qui héberge le runner.
Pourquoi les tests visuels passent en local mais cassent en CI
Le navigateur qui rend votre référence sur macOS n'est pas celui qui rend la comparaison dans un conteneur CI Linux. La documentation de Playwright est catégorique sur ce point : le rendu du navigateur peut varier selon l'OS hôte, la version, les réglages, le matériel, la source d'alimentation, le mode headless et d'autres facteurs. Sa recommandation est tout aussi directe : pour des captures cohérentes, exécutez les tests dans l'environnement où les captures de référence ont été générées.
Cette seule recommandation justifie tout le reste de cette installation. Trois décalages expliquent la quasi-totalité des faux échecs :
- Différences de polices et d'anticrénelage. Votre machine de dev et le conteneur CI embarquent des paquets de polices et un rendu sous-pixel différents. Les pages riches en texte diffèrent à chaque exécution, même quand rien n'a changé.
- Rendu headless contre rendu avec interface. Un navigateur avec interface et un navigateur headless peuvent disposer la même page légèrement différemment.
- Épuisement de la mémoire partagée. C'est le tueur silencieux. Le /dev/shm par défaut de Docker fait 64MB, et Chromium headless utilise la mémoire partagée pour ses processus de rendu. Quand elle est épuisée, Chromium plante sans aucune erreur exploitable. Votre job meurt ou reste bloqué, tout simplement.
Les deux premiers se règlent en épinglant une seule image Docker pour la génération de la référence comme pour la comparaison. Le troisième se règle dans la section de configuration Docker ci-dessous. Corrigez les trois et les tests « instables en CI » deviennent déterministes.

Choisir votre outil : BackstopJS ou la comparaison intégrée de Playwright
Si vous utilisez déjà Playwright pour vos tests de bout en bout, le chemin le plus rapide est son assertion intégrée toHaveScreenshot() : aucune dépendance supplémentaire, comparaison au pixel près et références suffixées par plateforme dès l'installation. C'est un point d'arrêt légitime pour une petite suite, et pour une équipe sous la barre d'une cinquantaine d'écrans sans bibliothèque de composants partagée, cela peut suffire pour de bon.
Là où il atteint ses limites, c'est le flux de relecture. La comparaison de Playwright se fait uniquement au pixel et n'offre aucune UI de revue de PR : approuver un lot légitime de changements visuels revient donc à régénérer les instantanés à la main. Dès que le nombre de références grimpe, cette boucle d'approbation manuelle devient pénible.
BackstopJS est l'outil dédié conçu exactement pour cette boucle. Il est sous licence MIT (actuellement v6.3.25), pensé pour Docker via un flag --docker, et il produit du XML JUnit qu'un runner CI lit nativement. Son flux de référence (générer une référence, tester contre elle, approuver les écarts acceptés comme nouvelle référence) correspond au modèle opérationnel que finissent par vouloir la plupart des installations VRT auto-hébergées. Pour la suite de ce guide, BackstopJS sert d'exemple de travail.
Deux outils que vous verrez recommandés ailleurs sont écartés de cette démonstration : Argos n'est pas traité ici parce que son parcours produit public est centré sur l'application hébergée et les intégrations CI, tandis que Lost Pixel est archivé. BackstopJS reste l'exemple le plus sûr pour une installation auto-hébergée concrète.
| Axe | BackstopJS | Playwright toHaveScreenshot() |
|---|---|---|
| Flux de revue de PR | Rapport intégré et commande d'approbation | Régénération manuelle des instantanés |
| Gestion des références | Commandes reference / test / approve | Fichiers d'instantanés par test |
| Rendu normalisé par Docker | Flag --docker | Apportez votre propre image |
| Coût d'installation | Dépendance séparée | Déjà présent si vous utilisez Playwright |
Pour un comparatif plus poussé, sur davantage d'outils et de critères, notre comparatif de BackstopJS, Argos et Lost Pixel couvre l'ensemble de la décision de choix.
Déployer un runner CI auto-hébergé sur un VPS
La pièce qui apporte réellement la cohérence de rendu, c'est l'exécuteur Docker : le runner exécute chaque job dans un conteneur que vous définissez, ce qui rend l'environnement du navigateur identique à chaque fois. Choisissez la plateforme de runner selon tout ce que vous voulez lui faire faire en plus.
GitLab CE auto-hébergé est l'option la plus lourde : dépôts, CI/CD, registre, tickets et merge requests dans une seule instance. Ce qui compte pour ce guide, c'est l'exécuteur Docker de GitLab Runner, qui permet à chaque job de régression visuelle de s'exécuter dans la même image de conteneur épinglée à chaque fois. GitLab, Gitea, Jenkins, Forgejo, Portainer et Docker sont tous disponibles en déploiement en un clic sur la la marketplace Cloudzy, ce qui raccourcit considérablement la mise en place.
Gitea avec act-runner est l'alternative légère. Gitea est un service Git écrit en Go avec un moteur de workflows compatible GitHub Actions (via act-runner) : si votre équipe est à l'aise avec la syntaxe Actions, c'est un runner petit et rapide à monter.
Quel que soit votre choix, l'objectif de configuration est le même : un runner qui accepte des jobs et les exécute avec un exécuteur Docker. Tout ce qui suit (la config du conteneur, BackstopJS, le pipeline d'exemple) suppose cet exécuteur en place.
Configurer Docker pour que Chromium ne plante pas
Voici la panne qui coûte le plus de temps. Docker monte /dev/shm à 64MB par défaut. Chromium headless utilise cette partition de mémoire partagée pour ses processus de rendu, et lors d'une capture pleine page il lui en faut bien plus que 64MB. Quand il n'y en a plus, le moteur de rendu meurt, souvent sans aucun message d'erreur pointant vers la mémoire partagée. Le job se bloque, expire, ou signale un plantage de navigateur générique.
Il existe deux correctifs, et l'un comme l'autre fonctionne.
Correctif A : augmenter la taille de la mémoire partagée dans Compose. La clé shm_size définit la taille de la partition /dev/shm du conteneur. La référence du fichier Docker Compose confirme que shm_size configure la taille de la mémoire partagée autorisée par le conteneur du service. Définissez-la dans le bloc du service :
# docker-compose.yml
services:
vrt:
image: backstopjs/backstopjs:6.3.25
shm_size: '2gb' # override the 64MB default
volumes:
- ./:/src
working_dir: /src
Correctif B : dire à Chromium de ne pas utiliser /dev/shm du tout. Le flag --disable-dev-shm-usage fait écrire à Chromium ses fichiers de mémoire partagée dans /tmp plutôt que sur la partition de mémoire partagée. Passez-le dans les arguments de lancement du navigateur. Dans une config backstop.json, c'est engineOptions ; dans Playwright, c'est launchOptions :
// backstop.json (fragment)
{
"engine": "puppeteer",
"engineOptions": {
"args": ["--disable-dev-shm-usage", "--no-sandbox"]
}
}
Le correctif A est plus propre quand vous maîtrisez le fichier Compose ; le correctif B est le choix portable quand vous ne pouvez toucher qu'aux flags de lancement du navigateur. Appliquer les deux ne fait aucun mal.
Astuce de pro : Utilisez exactement la même image Docker en local et en CI. BackstopJS vous l'offre gratuitement avec le flag --docker, qui exécute la capture dans une image de référence épinglée pour que votre machine et le runner rendent à l'identique. C'est ce qui élimine les faux positifs liés aux polices et à l'anticrénelage : épinglez l'image une fois et arrêtez de courir après des écarts fantômes.

Configurer BackstopJS pour la CI
BackstopJS produit un rapport XML JUnit que votre runner CI lit pour faire passer ou échouer le build : c'est tout l'intérêt de le brancher dans un pipeline plutôt que de le lancer à la main. Installez-le et initialisez une config :
npm install --save-dev backstopjs
npx backstop init
Pointez ensuite backstop.json vers les pages à capturer et activez le rapport CI :
// backstop.json (fragment)
{
"id": "my_app_vrt",
"viewports": [
{ "label": "desktop", "width": 1440, "height": 900 }
],
"scenarios": [
{
"label": "Homepage",
"url": "http://app:3000/",
"selectors": ["document"]
}
],
"report": ["CI"],
"engine": "puppeteer",
"engineOptions": {
"args": ["--disable-dev-shm-usage", "--no-sandbox"]
}
}
C'est le réglage du rapport CI qui produit le XML JUnit. Utilisez le flag --docker quand vous lancez ces commandes en local depuis votre machine. Dans un job CI qui utilise déjà l'image backstopjs/backstopjs, exécutez plutôt backstop test directement. Le flux de référence tient en trois commandes :
npx backstop reference --docker # capture the approved baseline
npx backstop test --docker # run the comparison, emit the report
npx backstop approve # accept the current diffs as the new baseline
Une réserve sur la gestion des références : chaque changement d'UI légitime implique de lancer approve pour valider les nouvelles captures. Au-delà d'une centaine de scénarios environ, cette étape d'approbation devient un vrai coût opérationnel : quelqu'un doit examiner les écarts et décider lesquels sont voulus, et la charge augmente avec l'équipe qui l'adopte. Cette maintenance est le prix réel des tests de régression visuelle à grande échelle, et mieux vaut le budgéter avant d'engager une grosse suite.
Une configuration de pipeline CI qui fonctionne
Le pipeline se déroule en deux parties logiques : générer la référence une fois (ou à la demande), puis comparer à chaque changement. L'exemple ci-dessous est une configuration GitLab CI avec exécuteur Docker, où le correctif de mémoire partagée est déjà appliqué via les flags de lancement dans backstop.json.
# .gitlab-ci.yml
stages:
- visual-regression
visual_regression:
stage: visual-regression
image:
name: backstopjs/backstopjs:6.3.25
entrypoint: [""]
script:
- backstop test
artifacts:
when: always
reports:
junit: backstop_data/ci_report/xunit.xml
paths:
- backstop_data/bitmaps_test
- backstop_data/html_report
expire_in: 1 week
La ligne junit transmet le rapport à GitLab pour que les échecs apparaissent dans l'UI de la merge request ; la ligne paths conserve les bitmaps de différences pour que vous puissiez voir ce qui a réellement changé. Sur Gitea avec act-runner, les mêmes étapes se transposent dans un workflow de style Actions :
# .gitea/workflows/visual-regression.yaml
name: Visual Regression
on: [push, pull_request]
jobs:
vrt:
runs-on: ubuntu-latest
container:
image: backstopjs/backstopjs:6.3.25
options: --shm-size=2gb # raise /dev/shm for the job container
steps:
- uses: actions/checkout@v4
- run: backstop test
Notez l'option de conteneur --shm-size=2gb dans le workflow Gitea. C'est le même correctif de mémoire partagée que le correctif A, appliqué au niveau du conteneur du job, là où il n'y a pas de fichier Compose distinct à modifier.
Dimensionner votre VPS pour Chromium headless
La contrainte ici, c'est la RAM, pas le CPU. L'usage CPU de Chromium headless est en dents de scie (il grimpe pendant une capture et retombe entre deux), donc un nombre de cœurs modeste suffit largement. La mémoire est le plafond qui décide du nombre de jobs simultanés. Un processus Chromium headless occupe environ 300–500MB au repos et monte à 1–2GB pendant une capture pleine page : votre critère de dimensionnement est donc la RAM par job concurrent.
| RAM | Convient pour |
|---|---|
| 2GB | Minimum serré : un seul job séquentiel |
| 4GB | Base recommandée : 1–2 jobs concurrents |
| 8GB | 3–4 jobs concurrents |
| 16GB | Grosses suites et pipelines parallèles |
Ces chiffres sont des repères de praticiens tirés d'exécutions CI réelles, pas un benchmark de fournisseur : traitez-les comme des points de départ approximatifs et surveillez vos propres pics de mémoire.
À retenir dans cette section : 4GB de RAM constituent la base fiable ; ajoutez environ 2GB de marge par job de régression visuelle concurrent supplémentaire.
Le piège du VRT auto-hébergé, c'est qu'un runner à court de mémoire n'échoue pas bruyamment : il fait planter Chromium en silence, exactement le symptôme que toute cette installation cherche à éviter. Prévoir assez de marge est l'assurance la moins chère que vous puissiez souscrire. Si vous préférez ne pas monter un hôte de runner à partir de zéro, la marketplace de Cloudzy propose des déploiements en un clic pour GitLab auto-hébergé et Gitea auto-hébergé auto-hébergé, qui vous amènent à une instance CI opérationnelle en quelques minutes, sur un VPS que vous pouvez dimensionner exactement selon la marge dont votre suite a besoin pour Chromium headless.

Foire aux questions
Comment mettre en place des tests de régression visuelle dans un pipeline CI/CD ?
Installez BackstopJS, pointez backstop.json vers vos pages cibles et activez l'option de rapport CI pour produire du XML JUnit. Utilisez backstop reference --docker et backstop test --docker quand vous lancez depuis votre machine locale, pour que les captures se fassent dans l'image Docker épinglée. En CI, exécutez le job dans l'image backstopjs/backstopjs et appelez backstop test directement, puis publiez le rapport JUnit depuis backstop_data/ci_report/xunit.xml. Utilisez un runner auto-hébergé avec exécuteur Docker, dimensionné à au moins 4GB de RAM pour Chromium headless.
De quelle RAM Chromium headless a-t-il besoin dans un conteneur CI ?
Un processus Chromium headless utilise environ 300–500MB au repos et monte à 1–2GB pendant une capture pleine page. Pour une CI fiable, comptez environ 4GB de RAM pour un à deux jobs concurrents, en ajoutant environ 2GB par job parallèle supplémentaire. Le CPU est en dents de scie plutôt que limitant ; c'est la RAM qui détermine le nombre de jobs simultanés.
Pourquoi mes tests de régression visuelle passent-ils en local mais échouent-ils en CI ?
Le rendu du navigateur diffère selon les environnements : polices, anticrénelage, mode headless ou avec interface, et OS changent tous le résultat. La solution est de générer la référence et de faire la comparaison dans la même image Docker pour que les deux rendent à l'identique. Le flag --docker de BackstopJS fait exactement cela en exécutant les captures dans une image de référence épinglée.
Comment corriger l'erreur /dev/shm avec Chromium sous Docker en CI ?
Le /dev/shm par défaut de Docker fait 64MB, ce qui est trop peu pour le moteur de rendu de Chromium headless et provoque des plantages silencieux. Soit vous l'augmentez avec shm_size: '2gb' dans docker-compose.yml (ou --shm-size=2gb sur le conteneur du job), soit vous passez --disable-dev-shm-usage dans les flags de lancement de Chromium pour qu'il écrive ses fichiers de mémoire partagée dans /tmp.
Faut-il utiliser toHaveScreenshot de Playwright ou un outil de régression visuelle dédié ?
Le toHaveScreenshot() intégré de Playwright suffit pour les petites suites : moins d'une cinquantaine d'écrans environ, sans bibliothèque de composants partagée et sans besoin d'UI de revue de PR. Passez à un outil dédié comme BackstopJS quand il vous faut un vrai flux d'approbation des références, un rapport de revue et un rendu normalisé par Docker, ce qui devient important dès que le nombre de références augmente.
Lost Pixel est-il toujours maintenu ?
Non. Lost Pixel a été archivé en avril 2026 et n'est pas un candidat pour de nouveaux déploiements. Utilisez BackstopJS pour une installation de tests de régression visuelle auto-hébergée.