Seus testes de regressão visual passam no notebook. Você faz push, o pipeline roda, e os mesmos testes ou derrubam o navegador em silêncio ou apontam cinquenta "mudanças" que são só diferenças de anti-aliasing. Nada mudou no seu código, mas a CI discorda.
Essa distância entre "funciona local" e "funciona na CI" quase sempre é um problema de infraestrutura, não de teste. Para hospedar testes de regressão visual em um pipeline de CI de forma confiável, você precisa de quatro peças ligadas corretamente: um runner de CI com executor Docker, um contêiner configurado para o Chromium headless não ficar sem memória compartilhada, uma ferramenta de diff que emita relatórios legíveis pela CI, e um VPS dimensionado para a memória que navegadores headless realmente consomem.
Este guia monta essa stack de ponta a ponta. No final você terá um .gitlab-ci.yml funcionando (ou o equivalente no Gitea), um ambiente Docker que impede o Chromium de travar, o BackstopJS gerando relatórios JUnit que seu pipeline consegue ler, e um VPS do tamanho certo para rodar tudo isso.
TL;DR
- Rode o navegador na mesma imagem Docker local e na CI. A maioria dos diffs falso-positivos vem de diferenças de fonte e renderização entre ambientes. Fixe a imagem e eles somem.
- Corrija o padrão de 64MB do /dev/shm. O tamanho padrão de memória compartilhada do Docker deixa o renderizador do Chromium sem recursos, e ele trava em silêncio na CI. Defina shm_size: '2gb' no docker-compose.yml, ou passe --disable-dev-shm-usage para o Chromium.
- Use o BackstopJS como ferramenta principal auto-hospedável. Ele tem licença MIT (v6.3.25), é Docker-first pela flag --docker e emite relatórios JUnit. Se você já usa Playwright, o toHaveScreenshot() nativo dele é um bom ponto de partida sem instalar nada.
- Dimensione para 4GB de RAM como base. Seja generoso com memória. Em execuções reais de CI, um job de screenshot no Chromium pode consumir muito mais memória durante capturas de página inteira do que parece precisar em repouso, então 4GB de RAM é a base mais segura para um ou dois jobs sequenciais ou com pouco paralelismo.
O Que Este Guia Não Cobre
Este é um guia de construção para uma infraestrutura que você já decidiu subir. Algumas coisas ficam de fora de propósito:
- Não é um comparativo completo de ferramentas. Este guia foca na stack que você consegue de fato operar em um ambiente de CI auto-hospedado: BackstopJS, Docker e seu próprio runner.
- Não cobre a auto-hospedagem do Argos. O Argos é open source, mas o fluxo público do produto e a documentação focam no app Argos hospedado e nas integrações de CI, e não em um caminho simples de auto-hospedagem em produção. Para o objetivo deste guia, o BackstopJS é o exemplo prático mais seguro.
- Não recomenda o Lost Pixel. Esse projeto foi arquivado em abril de 2026; use o BackstopJS para novas implantações.
- Não refaz o debate entre auto-hospedagem e SaaS gerenciado. Se você está aqui, já descartou Percy ou Chromatic.
Pré-requisitos
Você vai precisar de algumas coisas prontas antes do primeiro comando:
- Um runner de CI auto-hospedado, ou um plano para implantar um (visto adiante).
- Docker instalado no host do runner.
- Um projeto Node.js com um alvo de teste existente (a URL de uma aplicação no ar ou um conjunto de rotas de componentes para capturar).
- Acesso ao shell do VPS ou host onde o runner roda.
Por que os testes visuais passam localmente e quebram na CI
O navegador que renderiza sua referência no macOS não é o navegador que renderiza a comparação em um contêiner Linux na CI. A própria documentação do Playwright é direta sobre isso: a renderização do navegador pode variar conforme o OS do host, versão, configurações, hardware, fonte de energia, modo headless e outros fatores. A recomendação é igualmente direta: para screenshots consistentes, execute os testes no mesmo ambiente em que as capturas de referência foram geradas.
Essa única recomendação é a razão de existir todo o resto desta configuração. Três descompassos explicam quase todas as falhas falsas:
- Diferenças de fonte e anti-aliasing. Sua máquina de desenvolvimento e o contêiner da CI trazem pacotes de fontes e renderização sub-pixel diferentes. Páginas com muito texto acusam diff a cada execução, mesmo sem nenhuma alteração.
- Renderização headless versus com interface. Um navegador com interface e um headless podem diagramar a mesma página de forma ligeiramente diferente.
- Esgotamento da memória compartilhada. Esse é o silencioso. O /dev/shm padrão do Docker é de 64MB, e o Chromium headless usa memória compartilhada para os processos de renderização. Quando ela acaba, o Chromium trava sem nenhum erro útil. Seu job simplesmente morre ou fica travado.
Os dois primeiros se resolvem fixando uma única imagem Docker tanto para gerar a referência quanto para comparar. O terceiro se resolve na seção de configuração do Docker, mais abaixo. Corrija os três e os testes que eram "instáveis na CI" ficam determinísticos.

Escolhendo a ferramenta: BackstopJS ou o diff nativo do Playwright
Se você já usa Playwright para testes ponta a ponta, o caminho mais rápido é a asserção nativa toHaveScreenshot(): zero dependências extras, diff no nível do pixel e referências com sufixo por plataforma de fábrica. É um ponto de parada legítimo para uma suíte pequena, e para times com menos de cerca de cinquenta telas e sem biblioteca de componentes compartilhada, pode ser tudo de que você precisa.
Onde ele chega ao limite é no fluxo de revisão. O diff do Playwright é só por pixel e não tem UI de revisão de PR, então aprovar um lote legítimo de mudanças visuais significa regerar snapshots na mão. Conforme o número de referências cresce, esse ciclo manual de aprovação vira um sofrimento.
BackstopJS é a ferramenta dedicada, feita exatamente para esse ciclo. Tem licença MIT (atualmente v6.3.25), é Docker-first pela flag --docker e emite JUnit XML que um runner de CI lê nativamente. Seu fluxo de referência (gerar uma referência, testar contra ela, aprovar os diffs aceitos como a nova referência) é o modelo operacional que a maioria dos setups de VRT auto-hospedados acaba querendo. No resto deste guia, o BackstopJS é o exemplo prático.
Duas ferramentas que você pode ver recomendadas em outros lugares ficam de fora deste passo a passo: o Argos não é coberto aqui porque seu fluxo público de produto gira em torno do app hospedado e das integrações de CI, e o Lost Pixel está arquivado. O BackstopJS é o exemplo prático mais seguro para um setup auto-hospedado real.
| Eixo | BackstopJS | Playwright toHaveScreenshot() |
|---|---|---|
| Fluxo de revisão de PR | Relatório nativo e comando de aprovação | Regeração manual de snapshots |
| Gerenciamento de referências | Comandos reference / test / approve | Arquivos de snapshot por teste |
| Renderização normalizada por Docker | Flag --docker | Traga a sua própria imagem |
| Custo de instalação | Dependência separada | Já presente se você usa Playwright |
Para uma disputa mais aprofundada, com mais ferramentas e critérios, nosso comparativo entre BackstopJS, Argos e Lost Pixel cobre toda a decisão de escolha.
Implantando um runner de CI próprio em um VPS
A peça que realmente entrega consistência de renderização é o executor Docker: o runner roda cada job dentro de um contêiner que você define, o que significa que o ambiente do navegador é idêntico toda vez. Escolha a plataforma de runner conforme quanto mais você quer que ela faça.
O GitLab CE auto-hospedado é a opção mais pesada: repositórios, CI/CD, registry, issues e merge requests em uma única instância. A parte importante para este guia é o executor Docker do GitLab Runner, que faz cada job de regressão visual rodar sempre dentro da mesma imagem de contêiner fixada. GitLab, Gitea, Jenkins, Forgejo, Portainer e Docker estão todos disponíveis como implantações em um clique no o marketplace da Cloudzy, o que encurta bastante a configuração.
Gitea com act-runner é a alternativa leve. O Gitea é um serviço Git escrito em Go com um motor de workflows compatível com GitHub Actions (via act-runner), então, se seu time já está à vontade com a sintaxe do Actions, é um runner pequeno e rápido de subir.
Qualquer que seja a escolha, o objetivo da configuração é o mesmo: um runner que aceita jobs e os executa com um executor Docker. Tudo o que vem depois (a configuração do contêiner, o BackstopJS, o pipeline de exemplo) parte do princípio de que esse executor já existe.
Configurando o Docker para o Chromium não travar
Aqui está a falha que mais custa tempo às pessoas. O Docker monta o /dev/shm com 64MB por padrão. O Chromium headless usa essa partição de memória compartilhada para os processos de renderização, e durante um screenshot de página inteira ele precisa de muito mais que 64MB. Quando ela acaba, o renderizador morre, muitas vezes sem nenhuma mensagem de erro que aponte para memória compartilhada. O job trava, estoura o tempo limite ou reporta uma falha genérica do navegador.
Existem duas correções, e qualquer uma delas funciona.
Correção A: aumente o tamanho da memória compartilhada no Compose. A chave shm_size define o tamanho da partição /dev/shm do contêiner. A referência do arquivo Docker Compose confirma que shm_size configura o tamanho da memória compartilhada permitida ao contêiner do serviço. Defina isso no bloco do serviço:
# docker-compose.yml
services:
vrt:
image: backstopjs/backstopjs:6.3.25
shm_size: '2gb' # override the 64MB default
volumes:
- ./:/src
working_dir: /src
Correção B: diga ao Chromium para não usar o /dev/shm. A flag --disable-dev-shm-usage faz o Chromium gravar os arquivos de memória compartilhada em /tmp em vez da partição de memória compartilhada. Passe-a nos argumentos de inicialização do navegador. Em uma configuração backstop.json, isso é engineOptions; no Playwright, é launchOptions:
// backstop.json (fragment)
{
"engine": "puppeteer",
"engineOptions": {
"args": ["--disable-dev-shm-usage", "--no-sandbox"]
}
}
A correção A é mais limpa quando você controla o arquivo Compose; a correção B é a escolha portátil quando você só consegue mexer nas flags de inicialização do navegador. Aplicar as duas não faz mal.
Dica profissional: Use exatamente a mesma imagem Docker local e na CI. O BackstopJS entrega isso de graça com a flag --docker, que roda a captura dentro de uma imagem de referência fixada para que sua máquina e o runner rendam de forma idêntica. É isso que elimina os falso-positivos de fonte e anti-aliasing: fixe a imagem uma vez e pare de perseguir diffs fantasmas.

Configurando o BackstopJS para a CI
O BackstopJS gera um relatório JUnit XML que seu runner de CI lê para aprovar ou reprovar o build, que é exatamente o motivo de ligá-lo a um pipeline em vez de rodar na mão. Instale e inicialize uma configuração:
npm install --save-dev backstopjs
npx backstop init
Depois aponte o backstop.json para as páginas que você quer capturar e ative o relatório de 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"]
}
}
A configuração do relatório de CI é o que emite o JUnit XML. Use a flag --docker ao rodar esses comandos localmente, a partir da sua máquina. Dentro de um job de CI que já usa a imagem backstopjs/backstopjs, rode backstop test diretamente. O fluxo de referência são três comandos:
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
Uma ressalva sobre o gerenciamento de referências: toda mudança legítima de UI significa rodar approve para abençoar as novas capturas. Passando de uns cem cenários, essa etapa de aprovação vira um custo operacional real: alguém precisa olhar os diffs e decidir quais são intencionais, e o peso cresce junto com o time que adota a prática. Essa manutenção é o preço real do teste de regressão visual em escala, e vale contabilizá-la antes de comprometer uma suíte grande.
Uma configuração de pipeline de CI que funciona
O pipeline roda em duas partes lógicas: gerar a referência uma vez (ou sob demanda) e depois comparar com ela a cada mudança. O exemplo abaixo é uma configuração do GitLab CI usando um executor Docker, com a correção de memória compartilhada já aplicada pelas flags de inicialização no 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
A linha junit entrega o relatório ao GitLab para que as falhas apareçam na UI do merge request; a linha paths guarda os bitmaps de diff para você ver o que realmente mudou. No Gitea com act-runner, os mesmos passos viram um workflow no estilo 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
Repare na opção de contêiner --shm-size=2gb no workflow do Gitea. É a mesma correção de memória compartilhada da correção A, aplicada no nível do contêiner do job, onde não há um arquivo Compose separado para editar.
Dimensionando seu VPS para o Chromium headless
A restrição aqui é a RAM, não a CPU. O uso de CPU do Chromium headless é em rajadas (dispara durante uma captura e fica ocioso entre elas), então uma contagem modesta de núcleos dá conta. A memória é o teto que decide quantos jobs você consegue rodar ao mesmo tempo. Um processo do Chromium headless fica em cerca de 300–500MB em repouso e sobe para 1–2GB ao capturar um screenshot de página inteira, então o que guia seu dimensionamento é a RAM por job simultâneo.
| RAM | Indicado para |
|---|---|
| 2GB | Mínimo apertado: um único job sequencial |
| 4GB | Base recomendada: 1–2 jobs simultâneos |
| 8GB | 3–4 jobs simultâneos |
| 16GB | Suítes grandes e pipelines paralelos |
Esses números são orientação de quem opera na prática, tirados de execuções reais de CI, não um benchmark de fornecedor, então trate-os como pontos de partida aproximados e acompanhe o seu próprio pico de memória.
Conclusão da seção: 4GB de RAM é a base confiável; some cerca de 2GB de folga para cada job simultâneo adicional de regressão visual.
O problema do VRT auto-hospedado é que um runner sem memória suficiente não falha de forma barulhenta: ele derruba o Chromium em silêncio, exatamente o sintoma que toda essa configuração existe para evitar. Provisionar folga suficiente é o seguro mais barato que você pode comprar. Se você prefere não montar um host de runner do zero, o marketplace da Cloudzy tem implantações em um clique para GitLab auto-hospedado e Gitea auto-hospedado que colocam uma instância de CI no ar em poucos minutos, em um VPS que você pode dimensionar exatamente para a folga de Chromium headless que sua suíte exige.

Perguntas frequentes
Como configurar testes de regressão visual em um pipeline de CI/CD?
Instale o BackstopJS, aponte o backstop.json para as páginas alvo e configure a opção de relatório de CI para emitir JUnit XML. Use backstop reference --docker e backstop test --docker ao rodar da sua máquina local, para que as capturas sejam renderizadas dentro da imagem Docker fixada. Na CI, rode o job dentro da imagem backstopjs/backstopjs e chame backstop test diretamente, depois publique o relatório JUnit de backstop_data/ci_report/xunit.xml. Use um runner auto-hospedado com executor Docker, dimensionado com pelo menos 4GB de RAM para o Chromium headless.
Quanta RAM o Chromium headless precisa em um contêiner de CI?
Um processo do Chromium headless usa cerca de 300–500MB em repouso e dispara para 1–2GB ao capturar um screenshot de página inteira. Para uma CI confiável, conte com uns 4GB de RAM para um ou dois jobs simultâneos, somando cerca de 2GB para cada job paralelo adicional. A CPU trabalha em rajadas e não é o gargalo; a RAM é o que determina quantos jobs você consegue rodar ao mesmo tempo.
Por que meus testes de regressão visual passam localmente e falham na CI?
A renderização do navegador varia entre ambientes: fontes, anti-aliasing, modo headless versus com interface e o OS mudam a saída. A correção é rodar a geração da referência e a comparação na mesma imagem Docker, para que as duas rendam de forma idêntica. A flag --docker do BackstopJS faz isso executando as capturas dentro de uma imagem de referência fixada.
Como corrigir o erro de /dev/shm no Chromium em Docker na CI?
O /dev/shm padrão do Docker é de 64MB, pequeno demais para o renderizador do Chromium headless, e causa travamentos silenciosos. Ou você aumenta com shm_size: '2gb' no docker-compose.yml (ou --shm-size=2gb no contêiner do job), ou passa --disable-dev-shm-usage nas flags de inicialização do Chromium para que ele grave os arquivos de memória compartilhada em /tmp.
Devo usar o Playwright toHaveScreenshot ou uma ferramenta dedicada de regressão visual?
O toHaveScreenshot() nativo do Playwright basta para suítes pequenas: menos de cerca de cinquenta telas, sem biblioteca de componentes compartilhada e sem necessidade de uma UI de revisão de PR. Passe para uma ferramenta dedicada como o BackstopJS quando precisar de um fluxo decente de aprovação de referências, um relatório de revisão e renderização normalizada por Docker, o que passa a importar quando o número de referências cresce.
O Lost Pixel ainda é mantido?
Não. O Lost Pixel foi arquivado em abril de 2026 e não é candidato para novas implantações. Use o BackstopJS para montar testes de regressão visual auto-hospedados.