I tuoi test di regressione visiva passano sul portatile. Fai push, la pipeline parte e gli stessi test o mandano in crash il browser senza dire nulla, oppure segnalano cinquanta «modifiche» che sono solo differenze di anti-aliasing. Nel codice non è cambiato niente, ma la CI la pensa diversamente.
Quel divario tra «funziona in locale» e «funziona in CI» è quasi sempre un problema di infrastruttura, non di test. Per self-hostare i test di regressione visiva in una pipeline CI in modo affidabile servono quattro pezzi collegati bene tra loro: un runner CI con executor Docker, un container configurato in modo che Chromium headless non esaurisca la memoria condivisa, uno strumento di diff impostato per produrre report leggibili dalla CI e un VPS dimensionato sulla memoria che i browser headless consumano davvero.
Questa guida costruisce quello stack dall'inizio alla fine. Alla fine avrai un .gitlab-ci.yml funzionante (o l'equivalente per Gitea), un ambiente Docker che impedisce a Chromium di andare in crash, BackstopJS che produce report JUnit leggibili dalla tua pipeline e un VPS dimensionato correttamente su cui far girare il tutto.
TL;DR
- Esegui il browser nella stessa immagine Docker in locale e in CI. La maggior parte dei falsi positivi nei diff nasce da differenze di font e rendering tra ambienti. Fissa l'immagine e spariscono.
- Correggi il valore predefinito di 64MB per /dev/shm. La dimensione predefinita della memoria condivisa di Docker affama il renderer di Chromium, che va in crash silenziosamente in CI. Imposta shm_size: '2gb' in docker-compose.yml, oppure passa --disable-dev-shm-usage a Chromium.
- Usa BackstopJS come strumento self-hosted principale. È sotto licenza MIT (v6.3.25), pensato prima di tutto per Docker grazie al flag --docker, e produce report JUnit. Se usi già Playwright, il suo toHaveScreenshot() integrato è un ottimo punto di partenza a installazione zero.
- Parti da 4GB di RAM come riferimento. Prevedi memoria con generosità. Nelle esecuzioni CI reali, un job di screenshot con Chromium può consumare durante le catture a pagina intera molta più memoria di quanta sembri servirgli a riposo, quindi 4GB di RAM sono la base più sicura per uno o due job sequenziali o leggermente paralleli.
Cosa non copre questa guida
Questa è una guida realizzativa per un'infrastruttura che hai già deciso di mettere in piedi. Alcune cose restano volutamente fuori:
- Non è un confronto completo tra strumenti. Questa guida si concentra sullo stack che puoi davvero gestire in un ambiente CI self-hosted: BackstopJS, Docker e il tuo runner.
- Non tratta il self-hosting di Argos. Argos è open source, ma il suo flusso di prodotto pubblico e la documentazione si concentrano sull'app Argos ospitata e sulle integrazioni CI, non su un percorso semplice di self-hosting in produzione. Per l'obiettivo di questa guida, BackstopJS è l'esempio pratico più sicuro.
- Non consiglia Lost Pixel. Quel progetto è stato archiviato ad aprile 2026; per i nuovi deployment usa BackstopJS.
- Non rimette in discussione self-hosting contro SaaS gestito. Se sei qui, hai già escluso Percy o Chromatic.
Prerequisiti
Prima del primo comando ti servono alcune cose:
- Un runner CI self-hosted, o l'intenzione di installarne uno (vedi sotto).
- Docker installato sull'host del runner.
- Un progetto Node.js con un target di test già esistente (l'URL di un'app in esecuzione o un insieme di route di componenti da catturare).
- Accesso shell al VPS o all'host dove risiede il runner.
Perché i test visivi passano in locale ma si rompono in CI
Il browser che genera la tua baseline su macOS non è lo stesso che genera il confronto dentro un container CI Linux. La documentazione di Playwright è esplicita: il rendering del browser può variare in base a OS dell'host, versione, impostazioni, hardware, alimentazione, modalità headless e altri fattori. La raccomandazione è altrettanto diretta: per screenshot coerenti, esegui i test nello stesso ambiente in cui sono state generate le immagini di riferimento.
Quella sola raccomandazione è il motivo per cui esiste tutto il resto di questa configurazione. Tre disallineamenti spiegano quasi ogni falso fallimento:
- Differenze di font e anti-aliasing. La tua macchina di sviluppo e il container CI includono pacchetti di font e rendering sub-pixel diversi. Le pagine ricche di testo producono diff a ogni esecuzione anche quando non è cambiato nulla.
- Rendering headless contro rendering con interfaccia. Un browser con interfaccia e uno headless possono impaginare la stessa pagina in modo leggermente diverso.
- Esaurimento della memoria condivisa. Questo è quello silenzioso. Il /dev/shm predefinito di Docker è di 64MB, e Chromium headless usa la memoria condivisa per i suoi processi di rendering. Quando finisce, Chromium va in crash senza alcun errore utile. Il tuo job semplicemente muore o si blocca.
I primi due si risolvono fissando un'unica immagine Docker sia per la generazione della baseline sia per il confronto. Il terzo si risolve nella sezione sulla configurazione di Docker qui sotto. Sistema tutti e tre e i test «instabili in CI» diventano deterministici.

Scegliere lo strumento: BackstopJS o il diff integrato di Playwright
Se usi già Playwright per i test end-to-end, la strada più rapida è la sua asserzione integrata toHaveScreenshot(): zero dipendenze aggiuntive, confronto a livello di pixel e baseline con suffisso di piattaforma già pronte. È un punto d'arrivo legittimo per una suite piccola, e per team con meno di una cinquantina di schermate e senza libreria di componenti condivisa potrebbe bastare per sempre.
Dove si ferma è il flusso di revisione. Il diff di Playwright è solo a livello di pixel e non ha una UI di revisione delle PR, quindi approvare un gruppo legittimo di modifiche visive significa rigenerare gli snapshot a mano. Quando il numero di baseline cresce, quel ciclo di approvazione manuale diventa doloroso.
BackstopJS è lo strumento dedicato costruito esattamente per quel ciclo. È sotto licenza MIT (attualmente v6.3.25), pensato prima di tutto per Docker grazie al flag --docker, e produce XML JUnit che un runner CI legge in modo nativo. Il suo flusso basato sulla baseline (genera un riferimento, testa rispetto a quello, approva i diff accettati come nuova baseline) è il modello operativo che la maggior parte delle configurazioni VRT self-hosted finisce per volere. Per il resto di questa guida, BackstopJS è l'esempio di riferimento.
Due strumenti che potresti vedere consigliati altrove restano fuori da questa guida pratica: Argos non viene trattato perché il suo flusso di prodotto pubblico ruota attorno all'app ospitata e alle integrazioni CI, mentre Lost Pixel è archiviato. BackstopJS è l'esempio più sicuro per una configurazione self-hosted concreta.
| Asse | BackstopJS | Playwright toHaveScreenshot() |
|---|---|---|
| Flusso di revisione delle PR | Report integrato e comando di approvazione | Rigenerazione manuale degli snapshot |
| Gestione delle baseline | Comandi reference / test / approve | File di snapshot per singolo test |
| Rendering normalizzato con Docker | flag --docker | Porta la tua immagine |
| Costo di installazione | Dipendenza separata | Già presente se usi Playwright |
Per un confronto più approfondito su più strumenti e criteri, il nostro confronto tra BackstopJS, Argos e Lost Pixel copre l'intera decisione di scelta.
Installare un runner CI self-hosted su un VPS
Il pezzo che garantisce davvero la coerenza del rendering è l'executor Docker: il runner esegue ogni job dentro un container che definisci tu, quindi l'ambiente del browser è identico ogni volta. Scegli la piattaforma del runner in base a quante altre cose vuoi che faccia.
GitLab CE self-hosted è l'opzione più pesante: repository, CI/CD, registry, issue e merge request in una sola istanza. La parte importante per questa guida è l'executor Docker di GitLab Runner, che permette a ogni job di regressione visiva di girare ogni volta nella stessa immagine container fissata. GitLab, Gitea, Jenkins, Forgejo, Portainer e Docker sono tutti disponibili come deployment in un clic sul il marketplace di Cloudzy, il che accorcia parecchio la configurazione.
Gitea con act-runner è l'alternativa leggera. Gitea è un servizio Git scritto in Go con un motore di workflow compatibile con GitHub Actions (tramite act-runner), quindi se il tuo team ha dimestichezza con la sintassi di Actions è un runner piccolo e veloce da mettere in piedi.
Qualunque tu scelga, l'obiettivo della configurazione è lo stesso: un runner che accetta job e li esegue con un executor Docker. Tutto quello che viene dopo (la configurazione del container, BackstopJS, la pipeline di esempio) dà per scontato che quell'executor ci sia.
Configurare Docker perché Chromium non vada in crash
Ecco il guasto che fa perdere più tempo. Docker monta /dev/shm a 64MB per impostazione predefinita. Chromium headless usa quella partizione di memoria condivisa per i suoi processi di rendering e, durante uno screenshot a pagina intera, ha bisogno di molto più di 64MB. Quando finisce, il renderer muore, spesso senza alcun messaggio d'errore che punti alla memoria condivisa. Il job si blocca, va in timeout o segnala un generico crash del browser.
Ci sono due soluzioni, e vanno bene entrambe.
Soluzione A: aumenta la dimensione della memoria condivisa in Compose. La chiave shm_size imposta la dimensione della partizione /dev/shm del container. Il riferimento del file Docker Compose di Docker conferma che shm_size configura la quantità di memoria condivisa consentita al container del servizio. Impostala nel blocco del servizio:
# docker-compose.yml
services:
vrt:
image: backstopjs/backstopjs:6.3.25
shm_size: '2gb' # override the 64MB default
volumes:
- ./:/src
working_dir: /src
Soluzione B: di' a Chromium di non usare affatto /dev/shm. Il flag --disable-dev-shm-usage fa sì che Chromium scriva i file di memoria condivisa in /tmp invece che nella partizione dedicata. Passalo negli argomenti di avvio del browser. In una configurazione backstop.json si tratta di engineOptions; in Playwright è launchOptions:
// backstop.json (fragment)
{
"engine": "puppeteer",
"engineOptions": {
"args": ["--disable-dev-shm-usage", "--no-sandbox"]
}
}
La soluzione A è più pulita quando controlli il file Compose; la soluzione B è la scelta portabile quando puoi toccare solo i flag di avvio del browser. Applicarle entrambe non fa danni.
Consiglio: Usa esattamente la stessa immagine Docker in locale e in CI. BackstopJS te lo offre gratis con il flag --docker, che esegue la cattura dentro un'immagine di riferimento fissata, così la tua macchina e il runner producono lo stesso rendering. È questo che elimina i falsi positivi legati a font e anti-aliasing: fissa l'immagine una volta e smetti di rincorrere diff fantasma.

Configurare BackstopJS per la CI
BackstopJS produce un report XML JUnit che il tuo runner CI legge per far passare o fallire la build, ed è proprio il motivo per cui lo si collega a una pipeline invece di eseguirlo a mano. Installalo e inizializza una configurazione:
npm install --save-dev backstopjs
npx backstop init
Poi punta backstop.json alle pagine che vuoi catturare e attiva il report 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"]
}
}
L'impostazione del report CI è ciò che produce l'XML JUnit. Usa il flag --docker quando esegui questi comandi in locale dalla tua macchina. Dentro un job CI che già usa l'immagine backstopjs/backstopjs, esegui invece backstop test direttamente. Il flusso di lavoro basato sulla baseline è fatto di tre comandi:
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
Un avvertimento sulla gestione delle baseline: ogni modifica legittima alla UI comporta l'esecuzione di approve per benedire i nuovi screenshot. Superati all'incirca cento scenari, quel passaggio di approvazione diventa un costo operativo reale: qualcuno deve guardarsi i diff e decidere quali sono voluti, e il carico cresce con il team che lo adotta. Quella manutenzione è il vero prezzo dei test di regressione visiva su larga scala, e vale la pena metterla in conto prima di impegnarsi con una suite estesa.
Una configurazione di pipeline CI funzionante
La pipeline gira in due parti logiche: genera la baseline una volta (o su richiesta), poi confronta con essa a ogni modifica. L'esempio qui sotto è una configurazione GitLab CI che usa un executor Docker, con la correzione della memoria condivisa già applicata tramite i flag di avvio in 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 riga junit consegna il report a GitLab in modo che i fallimenti compaiano nella UI della merge request; la riga paths conserva le bitmap dei diff così puoi vedere cosa è davvero cambiato. Su Gitea con act-runner, gli stessi passaggi si traducono in un workflow in stile 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
Nota l'opzione container --shm-size=2gb nel workflow Gitea. È la stessa correzione della memoria condivisa della soluzione A, applicata a livello di container del job dove non c'è un file Compose separato da modificare.
Dimensionare il VPS per Chromium headless
Qui il vincolo è la RAM, non la CPU. L'uso di CPU di Chromium headless è a picchi (schizza durante una cattura e resta a riposo tra una e l'altra), quindi un numero modesto di core tiene botta senza problemi. La memoria è il tetto che decide quanti job puoi eseguire insieme. Un processo Chromium headless sta intorno ai 300–500MB a riposo e sale a 1–2GB mentre cattura uno screenshot a pagina intera, quindi il criterio di dimensionamento è la RAM per job concorrente.
| RAM | Adatto a |
|---|---|
| 2GB | Minimo risicato: un solo job sequenziale |
| 4GB | Base consigliata: 1–2 job concorrenti |
| 8GB | 3–4 job concorrenti |
| 16GB | Suite estese e pipeline parallele |
Queste cifre sono indicazioni pratiche ricavate da esecuzioni CI reali, non un benchmark di un fornitore, quindi trattale come punti di partenza approssimativi e tieni d'occhio i tuoi picchi di memoria.
Punto chiave della sezione: 4GB di RAM sono la base affidabile; aggiungi circa 2GB di margine per ogni ulteriore job di regressione visiva concorrente.
Il problema del VRT self-hosted è che un runner a corto di memoria non fallisce in modo evidente: manda in crash Chromium in silenzio, esattamente il sintomo che tutta questa configurazione serve a evitare. Prevedere abbastanza margine è l'assicurazione più economica che puoi comprare. Se preferisci non costruire un host per il runner da zero, il marketplace di Cloudzy offre deployment in un clic per GitLab self-hosted e i piani Gitea self-hosted self-hosted che ti portano a un'istanza CI funzionante in pochi minuti, su un VPS che puoi dimensionare esattamente sul margine di Chromium headless che serve alla tua suite.

Domande frequenti
Come si configurano i test di regressione visiva in una pipeline CI/CD?
Installa BackstopJS, punta backstop.json alle pagine di destinazione e imposta l'opzione del report CI per produrre XML JUnit. Usa backstop reference --docker e backstop test --docker quando lavori dalla tua macchina locale, così gli screenshot vengono generati dentro l'immagine Docker fissata. In CI, esegui il job dentro l'immagine backstopjs/backstopjs e chiama direttamente backstop test, poi pubblica il report JUnit da backstop_data/ci_report/xunit.xml. Usa un runner self-hosted con executor Docker, dimensionato ad almeno 4GB di RAM per Chromium headless.
Quanta RAM serve a Chromium headless in un container CI?
Un processo Chromium headless usa all'incirca 300–500MB a riposo e sale a 1–2GB mentre cattura uno screenshot a pagina intera. Per una CI affidabile, prevedi circa 4GB di RAM per uno o due job concorrenti, aggiungendo circa 2GB per ogni ulteriore job parallelo. La CPU lavora a picchi e non è il vincolo; è la RAM a determinare quanti job puoi eseguire insieme.
Perché i miei test di regressione visiva passano in locale ma falliscono in CI?
Il rendering del browser cambia da un ambiente all'altro: font, anti-aliasing, modalità headless o con interfaccia e OS modificano tutti il risultato. La soluzione è eseguire la generazione della baseline e il confronto nella stessa immagine Docker, così entrambi producono lo stesso rendering. Il flag --docker di BackstopJS fa esattamente questo, eseguendo le catture dentro un'immagine di riferimento fissata.
Come si corregge l'errore di /dev/shm in Chromium su Docker in CI?
Il /dev/shm predefinito di Docker è di 64MB, troppo poco per il renderer di Chromium headless, e causa crash silenziosi. Puoi alzarlo con shm_size: '2gb' in docker-compose.yml (oppure --shm-size=2gb sul container del job), oppure passare --disable-dev-shm-usage tra i flag di avvio di Chromium in modo che scriva i file di memoria condivisa in /tmp.
Meglio usare Playwright toHaveScreenshot o uno strumento dedicato di regressione visiva?
Il toHaveScreenshot() integrato di Playwright basta per suite piccole: meno di una cinquantina di schermate, nessuna libreria di componenti condivisa e nessun bisogno di una UI di revisione delle PR. Passa a uno strumento dedicato come BackstopJS quando ti serve un vero flusso di approvazione delle baseline, un report di revisione e un rendering normalizzato con Docker, cosa che diventa importante quando il numero di baseline cresce.
Lost Pixel è ancora mantenuto?
No. Lost Pixel è stato archiviato ad aprile 2026 e non è un candidato per nuovi deployment. Per una configurazione self-hosted di test di regressione visiva usa BackstopJS.