Dine visuelle regressionstests går igennem på din bærbare. Du pusher, pipelinen kører, og de samme tests får enten browseren til at gå ned i stilhed eller markerer halvtreds "ændringer", der i virkeligheden bare er forskelle i anti-aliasing. Intet i din kode er ændret, men CI er uenig.
Kløften mellem "virker lokalt" og "virker i CI" er næsten altid et infrastrukturproblem, ikke et testproblem. For at selv-hoste visuel regressionstest i en CI-pipeline på en pålidelig måde skal fire dele kobles rigtigt sammen: en CI-runner med en Docker-executor, en container konfigureret så headless Chromium ikke løber tør for delt hukommelse, et diff-værktøj sat op til at udsende CI-læsbare rapporter, og en VPS dimensioneret til den hukommelse, headless-browsere faktisk bruger.
Denne guide bygger hele stakken fra ende til anden. Til sidst har du en fungerende .gitlab-ci.yml (eller en tilsvarende til Gitea), et Docker-miljø der forhindrer Chromium i at gå ned, BackstopJS der producerer JUnit-rapporter, din pipeline kan læse, og en korrekt dimensioneret VPS at køre det hele på.
TL;DR
- Kør browseren i det samme Docker-image lokalt og i CI. De fleste falsk-positive diffs skyldes forskelle i skrifttyper og rendering mellem miljøer. Lås imaget fast, så forsvinder de.
- Ret standardværdien på 64MB for /dev/shm. Standardstørrelsen for delt hukommelse i Docker udsulter Chromiums renderer, som går ned i stilhed i CI. Sæt shm_size: '2gb' i docker-compose.yml, eller giv --disable-dev-shm-usage med til Chromium.
- Brug BackstopJS som det primære selv-hostbare værktøj. Det er MIT-licenseret (v6.3.25), Docker-først via et --docker-flag, og det udsender JUnit-rapporter. Kører du allerede Playwright, er dets indbyggede toHaveScreenshot() et fint udgangspunkt uden nogen installation.
- Regn med 4GB RAM som udgangspunkt. Vær rundhåndet med hukommelsen. I praktiske CI-kørsler kan et Chromium-screenshotjob bruge langt mere hukommelse under helsides-optagelser, end det ser ud til at have brug for i tomgang, så 4GB RAM er det sikreste udgangspunkt for et til to sekventielle eller let parallelle jobs.
Hvad denne guide ikke dækker
Dette er en byggeguide til infrastruktur, du allerede har besluttet dig for at sætte op. Nogle få ting er bevidst holdt udenfor:
- Det er ikke en fuld værktøjssammenligning. Denne guide fokuserer på den stak, du reelt kan drive i et selv-hostet CI-miljø: BackstopJS, Docker og din egen runner.
- Den dækker ikke selv-hosting af Argos. Argos er open source, men produktflowet og dokumentationen udadtil fokuserer på den hostede Argos-app og CI-integrationer frem for en enkel vej til selv-hosting i produktion. Til denne guides formål er BackstopJS det sikreste gennemarbejdede eksempel.
- Den anbefaler ikke Lost Pixel. Det projekt blev arkiveret i april 2026. Brug BackstopJS til nye deployments.
- Den tager ikke diskussionen om selv-hosting kontra managed SaaS forfra. Er du nået hertil, har du allerede valgt Percy eller Chromatic fra.
Forudsætninger
Et par ting skal være på plads, før du kører den første kommando:
- En selv-hostet CI-runner, eller en plan for at deploye en (dækkes nedenfor).
- Docker installeret på runner-værten.
- Et Node.js-projekt med et eksisterende testmål (en kørende app-URL eller et sæt komponentruter, der skal optages).
- Shell-adgang til den VPS eller vært, hvor runneren kører.
Hvorfor visuelle tests går igennem lokalt, men fejler i CI
Den browser, der renderer din baseline på macOS, er ikke den browser, der renderer sammenligningen i en Linux-CI-container. Playwrights egen dokumentation er ligeud ad landevejen: browser-rendering kan variere med værtens OS, version, indstillinger, hardware, strømkilde, headless-tilstand og andre faktorer. Anbefalingen er lige så direkte: for at få ensartede screenshots skal testene køre i samme miljø, som baseline-screenshots blev genereret i.
Netop den anbefaling er grunden til, at resten af denne opsætning findes. Tre uoverensstemmelser står bag stort set alle falske fejl:
- Forskelle i skrifttyper og anti-aliasing. Din udviklingsmaskine og CI-containeren leveres med forskellige skrifttypepakker og subpixel-rendering. Teksttunge sider giver diffs ved hver kørsel, selv når intet er ændret.
- Headless kontra headed rendering. En headed browser og en headless browser kan sætte den samme side lidt forskelligt op.
- Delt hukommelse løber tør. Det er den lydløse af slagsen. Dockers standard-/dev/shm er 64MB, og headless Chromium bruger delt hukommelse til sine renderer-processer. Når den løber tør, går Chromium ned uden nogen brugbar fejlmeddelelse. Dit job dør bare eller hænger.
De to første løses ved at låse ét Docker-image fast til både baseline-generering og sammenligning. Den tredje løses i afsnittet om Docker-konfiguration nedenfor. Ret alle tre, og de tests, der var "ustabile i CI", bliver deterministiske.

Valg af værktøj: BackstopJS eller Playwrights indbyggede diffing
Kører du allerede Playwright til end-to-end-tests, er den hurtigste vej den indbyggede toHaveScreenshot()-assertion: ingen ekstra afhængigheder, diffing på pixelniveau og baselines med platformsuffiks fra start. Det er et fuldt legitimt sted at stoppe for en lille testsuite, og for teams med under cirka halvtreds skærmbilleder og uden fælles komponentbibliotek er det måske alt, hvad du får brug for.
Der hvor det slipper op, er reviewflowet. Playwrights diffing er rent pixelbaseret og har ingen UI til PR-review, så at godkende en legitim portion visuelle ændringer betyder, at snapshots skal regenereres i hånden. Når antallet af baselines vokser, bliver den manuelle godkendelsesrunde smertefuld.
BackstopJS er det dedikerede værktøj bygget til præcis den arbejdsgang. Det er MIT-licenseret (aktuelt v6.3.25), Docker-først via et --docker-flag, og det udsender JUnit XML, som en CI-runner læser direkte. Dets baseline-workflow (generér en reference, test mod den, godkend accepterede diffs som ny baseline) er den driftsmodel, de fleste selv-hostede VRT-opsætninger ender med at ønske sig. I resten af denne guide er BackstopJS det gennemarbejdede eksempel.
To værktøjer, du måske ser anbefalet andre steder, er ude af billedet i denne gennemgang: Argos dækkes ikke her, fordi produktflowet udadtil er centreret om den hostede app og CI-integrationer, og Lost Pixel er arkiveret. BackstopJS er det sikreste gennemarbejdede eksempel til en praktisk selv-hostet opsætning.
| Akse | BackstopJS | Playwright toHaveScreenshot() |
|---|---|---|
| Workflow til PR-review | Indbygget rapport og approve-kommando | Manuel regenerering af snapshots |
| Håndtering af baselines | Kommandoerne reference / test / approve | Snapshot-filer pr. test |
| Docker-normaliseret rendering | --docker-flag | Tag dit eget image med |
| Installationsomkostning | Separat afhængighed | Allerede til stede, hvis du bruger Playwright |
Vil du have en dybere sammenligning på tværs af flere værktøjer og parametre, dækker vores sammenligning af BackstopJS, Argos og Lost Pixel hele valget.
Deployment af en selv-hostet CI-runner på en VPS
Den del, der reelt giver ensartet rendering, er Docker-executoren: runneren kører hvert job inde i en container, du selv definerer, hvilket betyder, at browsermiljøet er identisk hver gang. Vælg runner-platform ud fra, hvor meget andet den også skal kunne.
Selv-hostet GitLab CE er den tunge løsning: repositories, CI/CD, registry, issues og merge requests i én instans. Det vigtige i denne sammenhæng er GitLab Runners Docker-executor, som lader hvert visuelt regressionsjob køre i det samme fastlåste container-image hver gang. GitLab, Gitea, Jenkins, Forgejo, Portainer og Docker er alle tilgængelige som ét-kliks-deployments på Cloudzy-marketplacen, hvilket afkorter opsætningen betragteligt.
Gitea med act-runner er det lette alternativ. Gitea er en Go-baseret Git-tjeneste med en workflow-motor, der er kompatibel med GitHub Actions (via act-runner), så hvis dit team er fortroligt med Actions-syntaks, er det en lille og hurtig runner at få op at køre.
Uanset hvad du vælger, er målet med konfigurationen det samme: en runner, der tager imod jobs og udfører dem med en Docker-executor. Alt det efterfølgende (containerkonfigurationen, BackstopJS, eksempel-pipelinen) forudsætter, at den executor er på plads.
Sådan konfigurerer du Docker, så Chromium ikke går ned
Her er den fejl, der koster folk mest tid. Docker monterer som standard /dev/shm med 64MB. Headless Chromium bruger den partition med delt hukommelse til sine renderer-processer, og under et helsides-screenshot har den brug for langt mere end 64MB. Når den løber tør, dør rendereren, ofte uden nogen fejlmeddelelse, der peger på delt hukommelse. Jobbet hænger, timer ud eller melder om et generisk browsernedbrud.
Der findes to rettelser, og begge virker.
Rettelse A: hæv størrelsen på den delte hukommelse i Compose. Nøglen shm_size angiver størrelsen på containerens /dev/shm-partition. Docker Compose-filreferencen bekræfter, at shm_size konfigurerer størrelsen på den delte hukommelse, som servicecontaineren tillader. Sæt den i service-blokken:
# docker-compose.yml
services:
vrt:
image: backstopjs/backstopjs:6.3.25
shm_size: '2gb' # override the 64MB default
volumes:
- ./:/src
working_dir: /src
Rettelse B: bed Chromium om slet ikke at bruge /dev/shm. Flaget --disable-dev-shm-usage får Chromium til at skrive filer til delt hukommelse i /tmp i stedet for på partitionen til delt hukommelse. Giv det med i browserens opstartsargumenter. I en backstop.json-konfiguration er det engineOptions, i Playwright er det launchOptions:
// backstop.json (fragment)
{
"engine": "puppeteer",
"engineOptions": {
"args": ["--disable-dev-shm-usage", "--no-sandbox"]
}
}
Rettelse A er den reneste, når du selv styrer Compose-filen. Rettelse B er det portable valg, når du kun kan røre ved browserens opstartsflag. Det skader ikke at bruge begge.
Pro-tip: Brug præcis det samme Docker-image lokalt og i CI. BackstopJS giver dig det gratis med --docker-flaget, som kører optagelsen inde i et fastlåst reference-image, så din maskine og runneren renderer ens. Det er det, der fjerner de falske positiver fra skrifttyper og anti-aliasing: lås imaget fast én gang, og hold op med at jagte spøgelses-diffs.

Opsætning af BackstopJS til CI
BackstopJS producerer en JUnit XML-rapport, som din CI-runner læser for at afgøre, om builden består eller fejler. Det er hele pointen i at koble det ind i en pipeline frem for at køre det i hånden. Installér det, og initialisér en konfiguration:
npm install --save-dev backstopjs
npx backstop init
Peg derefter backstop.json på de sider, du vil optage, og slå CI-rapporten til:
// 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"]
}
}
Det er indstillingen for CI-rapporten, der udsender JUnit XML. Brug --docker-flaget, når du kører disse kommandoer lokalt fra din værtsmaskine. Inde i et CI-job, der allerede bruger backstopjs/backstopjs-imaget, skal du i stedet køre backstop test direkte. Baseline-workflowet består af tre kommandoer:
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
Et forbehold om håndtering af baselines: hver legitim UI-ændring betyder, at approve skal køres for at godkende de nye screenshots. Ud over cirka hundrede scenarier bliver det godkendelsestrin en reel driftsomkostning: nogen skal kigge diffs igennem og afgøre, hvilke der er tilsigtede, og byrden vokser i takt med, at teamet tager værktøjet i brug. Den vedligeholdelse er den reelle pris for visuel regressionstest i stor skala, og det er værd at budgettere med, før du kaster dig ud i en stor testsuite.
En fungerende CI-pipelinekonfiguration
Pipelinen kører i to logiske dele: generér baseline én gang (eller efter behov), og sammenlign derefter med den ved hver ændring. Eksemplet nedenfor er en GitLab CI-konfiguration med en Docker-executor, hvor rettelsen af den delte hukommelse allerede er anvendt via opstartsflagene i 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
junit-linjen afleverer rapporten til GitLab, så fejl dukker op i merge request-visningen. paths-linjen bevarer diff-bitmaps, så du kan se, hvad der faktisk blev ændret. På Gitea med act-runner oversættes de samme trin til et workflow i Actions-stil:
# .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
Bemærk container-indstillingen --shm-size=2gb i Gitea-workflowet. Det er den samme rettelse af delt hukommelse som rettelse A, blot anvendt på jobcontainer-niveau, hvor der ikke er nogen separat Compose-fil at redigere.
Dimensionering af din VPS til headless Chromium
Det er RAM, der er flaskehalsen her, ikke CPU. Headless Chromiums CPU-forbrug kommer i bølger (det topper under en optagelse og går i tomgang imellem), så et beskedent antal kerner følger fint med. Hukommelsen er loftet, der afgør, hvor mange jobs du kan køre samtidig. En headless Chromium-proces ligger på cirka 300–500MB i tomgang og stiger til 1–2GB, mens den optager et helsides-screenshot, så det er RAM pr. samtidigt job, der styrer dimensioneringen.
| RAM | Egnet til |
|---|---|
| 2GB | Stramt minimum: et enkelt sekventielt job |
| 4GB | Anbefalet udgangspunkt: 1–2 samtidige jobs |
| 8GB | 3–4 samtidige jobs |
| 16GB | Store testsuiter og parallelle pipelines |
Tallene er praktiske erfaringstal fra rigtige CI-kørsler, ikke et leverandør-benchmark, så betragt dem som omtrentlige udgangspunkter, og hold øje med dit eget hukommelsestop.
Hovedpointe i dette afsnit: 4GB RAM er det pålidelige udgangspunkt. Læg cirka 2GB ekstra til pr. yderligere samtidigt visuelt regressionsjob.
Hagen ved selv-hostet VRT er, at en runner med for lidt hukommelse ikke fejler højlydt, men lader Chromium gå ned i stilhed, præcis det symptom, som hele opsætningen findes for at undgå. At afsætte nok hukommelse er den billigste forsikring, du kan købe. Vil du helst ikke bygge en runner-host fra bunden, har marketplace hos Cloudzy ét-kliks udrulninger til selv-hostet GitLab og selv-hostet Gitea som giver dig en kørende CI-instans på få minutter, på en VPS du kan dimensionere til præcis den plads til headless Chromium, din testsuite kræver.

Ofte stillede spørgsmål
Hvordan sætter jeg visuel regressionstest op i en CI/CD-pipeline?
Installér BackstopJS, peg backstop.json på de sider, du vil teste, og sæt CI-rapportindstillingen til at udsende JUnit XML. Brug backstop reference --docker og backstop test --docker, når du kører fra din lokale maskine, så screenshots renderes inde i det fastlåste Docker-image. I CI skal jobbet køre inde i backstopjs/backstopjs-imaget og kalde backstop test direkte, hvorefter du publicerer JUnit-rapporten fra backstop_data/ci_report/xunit.xml. Brug en selv-hostet runner med en Docker-executor, dimensioneret til mindst 4GB RAM til headless Chromium.
Hvor meget RAM kræver headless Chromium i en CI-container?
En headless Chromium-proces bruger cirka 300–500MB i tomgang og topper på 1–2GB, mens den optager et helsides-screenshot. For pålidelig CI bør du regne med omkring 4GB RAM pr. et til to samtidige jobs og lægge cirka 2GB til for hvert ekstra parallelt job. CPU-forbruget kommer i bølger og er ikke flaskehalsen. Det er RAM, der afgør, hvor mange jobs du kan køre samtidig.
Hvorfor går mine visuelle regressionstests igennem lokalt, men fejler i CI?
Browser-rendering er forskellig fra miljø til miljø: skrifttyper, anti-aliasing, headless kontra headed tilstand og OS ændrer alle resultatet. Løsningen er at køre både baseline-generering og sammenligning i det samme Docker-image, så begge renderer ens. BackstopJS' --docker-flag gør netop det ved at køre optagelserne inde i et fastlåst reference-image.
Hvordan retter jeg /dev/shm-fejlen i Chromium under Docker-CI?
Dockers standard-/dev/shm er 64MB, hvilket er for lidt til headless Chromiums renderer og fører til lydløse nedbrud. Hæv den enten med shm_size: '2gb' i docker-compose.yml (eller --shm-size=2gb på jobcontaineren), eller giv --disable-dev-shm-usage med i Chromiums opstartsflag, så den i stedet skriver filer til delt hukommelse i /tmp.
Skal jeg bruge Playwright toHaveScreenshot eller et dedikeret værktøj til visuel regression?
Playwrights indbyggede toHaveScreenshot() er nok til små testsuiter: under cirka halvtreds skærmbilleder, uden fælles komponentbibliotek og uden behov for en UI til PR-review. Skift til et dedikeret værktøj som BackstopJS, når du har brug for et ordentligt workflow til godkendelse af baselines, en reviewrapport og Docker-normaliseret rendering, hvilket bliver vigtigt, når antallet af baselines vokser.
Bliver Lost Pixel stadig vedligeholdt?
Nej. Lost Pixel blev arkiveret i april 2026 og er ikke en kandidat til nye deployments. Brug BackstopJS til en selv-hostet opsætning af visuel regressionstest.