Tus pruebas de regresión visual pasan en tu portátil. Haces push, el pipeline se ejecuta y esas mismas pruebas o revientan el navegador en silencio o marcan cincuenta «cambios» que en realidad son solo diferencias de antialiasing. Nada cambió en tu código, pero CI opina lo contrario.
Esa distancia entre «funciona en local» y «funciona en CI» casi siempre es un problema de infraestructura, no de las pruebas. Para autoalojar pruebas de regresión visual en un pipeline de CI de forma fiable necesitas cuatro piezas bien conectadas: un runner de CI con ejecutor Docker, un contenedor configurado para que Chromium headless no se quede sin memoria compartida, una herramienta de diff que emita informes legibles por CI y un VPS dimensionado para la memoria que los navegadores headless consumen de verdad.
Esta guía construye ese stack de principio a fin. Al terminar tendrás un .gitlab-ci.yml funcionando (o su equivalente en Gitea), un entorno Docker que evita que Chromium se caiga, BackstopJS generando informes JUnit que tu pipeline puede leer y un VPS del tamaño adecuado para ejecutarlo todo.
TL;DR
- Ejecuta el navegador en la misma imagen de Docker en local y en CI. La mayoría de los diffs falsos positivos vienen de diferencias de fuentes y renderizado entre entornos. Fija la imagen y desaparecen.
- Corrige el valor por defecto de 64MB de /dev/shm. El tamaño de memoria compartida predeterminado de Docker deja sin recursos al renderizador de Chromium, que se cae en silencio en CI. Pon shm_size: '2gb' en docker-compose.yml o pasa --disable-dev-shm-usage a Chromium.
- Usa BackstopJS como herramienta autoalojable principal. Tiene licencia MIT (v6.3.25), es Docker-first mediante el flag --docker y emite informes JUnit. Si ya usas Playwright, su toHaveScreenshot() integrado es un buen punto de partida sin instalar nada.
- Dimensiona con 4GB de RAM como base. Sé generoso con la memoria. En ejecuciones reales de CI, un job de capturas con Chromium puede consumir mucha más memoria durante las capturas de página completa de la que parece necesitar en reposo, así que 4GB de RAM es la base más segura para uno o dos jobs secuenciales o con poco paralelismo.
Lo que esta guía no cubre
Esta es una guía de montaje para una infraestructura que ya has decidido levantar. Algunas cosas quedan fuera de forma deliberada:
- No es una comparativa completa de herramientas. Esta guía se centra en el stack que realmente puedes operar en un entorno de CI autoalojado: BackstopJS, Docker y tu propio runner.
- No cubre cómo autoalojar Argos. Argos es de código abierto, pero su flujo de producto público y su documentación se centran en la app alojada de Argos y en las integraciones con CI, no en un camino sencillo de autoalojamiento en producción. Para el objetivo de esta guía, BackstopJS es el ejemplo práctico más seguro.
- No recomienda Lost Pixel. Ese proyecto se archivó en abril de 2026; usa BackstopJS para despliegues nuevos.
- No vuelve a debatir autoalojamiento frente a SaaS gestionado. Si estás aquí, ya has descartado Percy o Chromatic.
Requisitos previos
Necesitas tener unas cuantas cosas listas antes del primer comando:
- Un runner de CI autoalojado, o un plan para desplegar uno (se explica más abajo).
- Docker instalado en el host del runner.
- Un proyecto Node.js con un objetivo de pruebas ya definido (la URL de una app en ejecución o un conjunto de rutas de componentes que capturar).
- Acceso por shell al VPS o host donde vive el runner.
Por qué las pruebas visuales pasan en local pero fallan en CI
El navegador que renderiza tu baseline en macOS no es el mismo que renderiza la comparación dentro de un contenedor Linux de CI. La propia documentación de Playwright lo dice sin rodeos: el renderizado del navegador puede variar según el OS del host, la versión, los ajustes, el hardware, la fuente de alimentación, el modo headless y otros factores. Su recomendación es igual de directa: para obtener capturas consistentes, ejecuta las pruebas en el mismo entorno donde se generaron las capturas de referencia.
Esa única recomendación es la razón de ser de todo lo que viene después. Tres desajustes explican casi todos los fallos falsos:
- Diferencias de fuentes y antialiasing. Tu máquina de desarrollo y el contenedor de CI traen paquetes de fuentes y renderizado subpíxel distintos. Las páginas con mucho texto dan diff en cada ejecución aunque no haya cambiado nada.
- Renderizado headless frente a headed. Un navegador con interfaz y uno headless pueden maquetar la misma página de forma ligeramente distinta.
- Agotamiento de la memoria compartida. Este es el silencioso. El /dev/shm por defecto de Docker es de 64MB, y Chromium headless usa memoria compartida para sus procesos de renderizado. Cuando se agota, Chromium se cae sin ningún error útil. Tu job simplemente muere o se queda colgado.
Los dos primeros se resuelven fijando una única imagen de Docker tanto para generar el baseline como para comparar. El tercero se resuelve en la sección de configuración de Docker más abajo. Corrige los tres y las pruebas que eran «inestables en CI» se vuelven deterministas.

Elegir herramienta: BackstopJS o el diff integrado de Playwright
Si ya usas Playwright para pruebas end-to-end, el camino más rápido es su aserción integrada toHaveScreenshot(): cero dependencias extra, diff a nivel de píxel y baselines con sufijo de plataforma desde el primer momento. Es un punto de parada legítimo para una suite pequeña, y para equipos con menos de unas cincuenta pantallas y sin librería de componentes compartida puede que sea todo lo que necesites.
Donde se queda corto es en el flujo de revisión. El diff de Playwright es solo de píxeles y no tiene UI de revisión de PR, así que aprobar un lote legítimo de cambios visuales implica regenerar los snapshots a mano. Cuando crece el número de baselines, ese ciclo manual de aprobación se vuelve doloroso.
BackstopJS es la herramienta específica creada justo para ese ciclo. Tiene licencia MIT (actualmente v6.3.25), es Docker-first mediante el flag --docker y emite JUnit XML que un runner de CI lee de forma nativa. Su flujo de baseline (generar una referencia, probar contra ella y aprobar los diffs aceptados como nuevo baseline) es el modelo operativo que acaban queriendo la mayoría de montajes de VRT autoalojados. En el resto de esta guía, BackstopJS es el ejemplo práctico.
Dos herramientas que quizá veas recomendadas en otros sitios quedan fuera de este recorrido: Argos no se cubre aquí porque su flujo de producto público gira en torno a la app alojada y las integraciones con CI, mientras que Lost Pixel está archivado. BackstopJS es el ejemplo práctico más seguro para un montaje autoalojado real.
| Eje | BackstopJS | Playwright toHaveScreenshot() |
|---|---|---|
| Flujo de revisión de PR | Informe integrado y comando de aprobación | Regeneración manual de snapshots |
| Gestión de baselines | Comandos reference / test / approve | Archivos de snapshot por prueba |
| Renderizado normalizado por Docker | Flag --docker | Trae tu propia imagen |
| Coste de instalación | Dependencia aparte | Ya está si usas Playwright |
Para una comparativa más a fondo con más herramientas y criterios, nuestra comparativa de BackstopJS, Argos y Lost Pixel cubre la decisión de selección completa.
Desplegar un runner de CI autoalojado en un VPS
La pieza que realmente aporta consistencia de renderizado es el ejecutor Docker: el runner ejecuta cada job dentro de un contenedor que tú defines, lo que significa que el entorno del navegador es idéntico siempre. Elige la plataforma del runner según cuántas otras cosas quieras que haga.
GitLab CE autoalojado es la opción pesada: repositorios, CI/CD, registro, incidencias y merge requests en una sola instancia. Lo importante para esta guía es el ejecutor Docker de GitLab Runner, que permite que cada job de regresión visual se ejecute siempre dentro de la misma imagen de contenedor fijada. GitLab, Gitea, Jenkins, Forgejo, Portainer y Docker están disponibles como despliegues en un clic en el el marketplace de Cloudzy, lo que acorta bastante la configuración.
Gitea con act-runner es la alternativa ligera. Gitea es un servicio Git escrito en Go con un motor de workflows compatible con GitHub Actions (vía act-runner), así que si tu equipo se maneja bien con la sintaxis de Actions, es un runner pequeño y rápido de levantar.
Elijas lo que elijas, el objetivo de la configuración es el mismo: un runner que acepte jobs y los ejecute con un ejecutor Docker. Todo lo que viene después (la configuración del contenedor, BackstopJS, el pipeline de ejemplo) da por hecho que ese ejecutor ya está.
Configurar Docker para que Chromium no se caiga
Este es el fallo que más tiempo hace perder. Docker monta /dev/shm con 64MB por defecto. Chromium headless usa esa partición de memoria compartida para sus procesos de renderizado, y durante una captura de página completa necesita mucho más de 64MB. Cuando se agota, el renderizador muere, a menudo sin ningún mensaje de error que apunte a la memoria compartida. El job se cuelga, agota el tiempo o informa de un fallo genérico del navegador.
Hay dos arreglos, y cualquiera de los dos funciona.
Arreglo A: subir el tamaño de memoria compartida en Compose. La clave shm_size establece el tamaño de la partición /dev/shm del contenedor. La referencia del archivo Docker Compose confirma que shm_size configura el tamaño de la memoria compartida permitida al contenedor del servicio. Ponlo en el bloque del servicio:
# docker-compose.yml
services:
vrt:
image: backstopjs/backstopjs:6.3.25
shm_size: '2gb' # override the 64MB default
volumes:
- ./:/src
working_dir: /src
Arreglo B: decirle a Chromium que no use /dev/shm en absoluto. El flag --disable-dev-shm-usage hace que Chromium escriba los archivos de memoria compartida en /tmp en lugar de en la partición de memoria compartida. Pásalo en los argumentos de arranque del navegador. En una configuración backstop.json es engineOptions; en Playwright es launchOptions:
// backstop.json (fragment)
{
"engine": "puppeteer",
"engineOptions": {
"args": ["--disable-dev-shm-usage", "--no-sandbox"]
}
}
El arreglo A es más limpio cuando controlas el archivo de Compose; el arreglo B es la opción portable cuando solo puedes tocar los flags de arranque del navegador. Aplicar ambos no hace daño.
Consejo pro: Usa exactamente la misma imagen de Docker en local y en CI. BackstopJS te lo da gratis con el flag --docker, que ejecuta la captura dentro de una imagen de referencia fijada para que tu máquina y el runner rendericen igual. Esto es lo que elimina los falsos positivos de fuentes y antialiasing: fija la imagen una vez y deja de perseguir diffs fantasma.

Configurar BackstopJS para CI
BackstopJS genera un informe JUnit XML que tu runner de CI lee para aprobar o suspender la build, que es justo el motivo de integrarlo en un pipeline en vez de ejecutarlo a mano. Instálalo e inicializa una configuración:
npm install --save-dev backstopjs
npx backstop init
Después apunta backstop.json a las páginas que quieres capturar y activa el informe 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"]
}
}
El ajuste del informe de CI es lo que emite el JUnit XML. Usa el flag --docker cuando ejecutes estos comandos en local desde tu máquina. Dentro de un job de CI que ya usa la imagen backstopjs/backstopjs, ejecuta backstop test directamente. El flujo de baseline son tres 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
Una advertencia sobre la gestión de baselines: cada cambio legítimo de UI implica ejecutar approve para dar el visto bueno a las nuevas capturas. Pasados unos cien escenarios, ese paso de aprobación es un coste operativo real: alguien tiene que revisar los diffs y decidir cuáles son intencionados, y la carga crece con el equipo que lo adopta. Ese mantenimiento es el precio real de las pruebas de regresión visual a escala, y conviene presupuestarlo antes de comprometer una suite grande.
Una configuración de pipeline de CI que funciona
El pipeline se ejecuta en dos partes lógicas: generar el baseline una vez (o bajo demanda) y luego comparar contra él en cada cambio. El ejemplo de abajo es una configuración de GitLab CI con ejecutor Docker, con el arreglo de memoria compartida ya aplicado mediante los flags de arranque en 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 línea junit entrega el informe a GitLab para que los fallos aparezcan en la UI de la merge request; la línea paths conserva los bitmaps de diff para que puedas ver qué cambió realmente. En Gitea con act-runner, los mismos pasos se traducen a un workflow al 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
Fíjate en la opción de contenedor --shm-size=2gb del workflow de Gitea. Es el mismo arreglo de memoria compartida que el arreglo A, aplicado a nivel del contenedor del job, donde no hay un archivo de Compose aparte que editar.
Dimensionar tu VPS para Chromium headless
Aquí el límite es la RAM, no la CPU. El uso de CPU de Chromium headless va a ráfagas (se dispara durante una captura y baja entre ellas), así que un número modesto de núcleos aguanta bien. La memoria es el techo que decide cuántos jobs puedes ejecutar a la vez. Un proceso de Chromium headless se queda en unos 300–500MB en reposo y sube a 1–2GB mientras captura una página completa, así que el factor de dimensionado es la RAM por job concurrente.
| RAM | Adecuado para |
|---|---|
| 2GB | Mínimo justo: un único job secuencial |
| 4GB | Base recomendada: 1–2 jobs concurrentes |
| 8GB | 3–4 jobs concurrentes |
| 16GB | Suites grandes y pipelines en paralelo |
Estas cifras son orientaciones de la práctica sacadas de ejecuciones reales de CI, no un benchmark de fabricante, así que tómalas como puntos de partida aproximados y vigila tu propio pico de memoria.
Idea clave de la sección: 4GB de RAM es la base fiable; añade unos 2GB de margen por cada job de regresión visual concurrente adicional.
El problema del VRT autoalojado es que un runner sin memoria suficiente no falla de forma ruidosa: tumba Chromium en silencio, exactamente el síntoma que toda esta configuración existe para evitar. Aprovisionar margen de sobra es el seguro más barato que puedes contratar. Si prefieres no montar un host de runner desde cero, el marketplace de Cloudzy tiene despliegues en un clic para GitLab autoalojado y Gitea autoalojado autoalojado que te dejan una instancia de CI funcionando en unos minutos, sobre un VPS que puedes dimensionar justo con el margen de Chromium headless que necesite tu suite.

Preguntas frecuentes
¿Cómo configuro pruebas de regresión visual en un pipeline de CI/CD?
Instala BackstopJS, apunta backstop.json a tus páginas objetivo y activa la opción de informe de CI para emitir JUnit XML. Usa backstop reference --docker y backstop test --docker cuando ejecutes desde tu máquina local para que las capturas se rendericen dentro de la imagen de Docker fijada. En CI, ejecuta el job dentro de la imagen backstopjs/backstopjs y llama a backstop test directamente, y luego publica el informe JUnit desde backstop_data/ci_report/xunit.xml. Usa un runner autoalojado con ejecutor Docker, dimensionado con al menos 4GB de RAM para Chromium headless.
¿Cuánta RAM necesita Chromium headless en un contenedor de CI?
Un proceso de Chromium headless usa unos 300–500MB en reposo y se dispara a 1–2GB mientras captura una página completa. Para un CI fiable, cuenta con unos 4GB de RAM por cada uno o dos jobs concurrentes, añadiendo unos 2GB por cada job paralelo adicional. La CPU va a ráfagas y no es el límite; la RAM es lo que determina cuántos jobs puedes ejecutar a la vez.
¿Por qué mis pruebas de regresión visual pasan en local pero fallan en CI?
El renderizado del navegador cambia según el entorno: las fuentes, el antialiasing, el modo headless frente al modo con interfaz y el OS alteran el resultado. La solución es generar el baseline y hacer la comparación en la misma imagen de Docker para que ambos rendericen igual. El flag --docker de BackstopJS hace justo eso, ejecutando las capturas dentro de una imagen de referencia fijada.
¿Cómo soluciono el error de /dev/shm con Chromium en CI sobre Docker?
El /dev/shm por defecto de Docker es de 64MB, demasiado poco para el renderizador de Chromium headless, y provoca caídas silenciosas. O lo subes con shm_size: '2gb' en docker-compose.yml (o --shm-size=2gb en el contenedor del job), o pasas --disable-dev-shm-usage en los flags de arranque de Chromium para que escriba los archivos de memoria compartida en /tmp.
¿Debo usar toHaveScreenshot de Playwright o una herramienta específica de regresión visual?
El toHaveScreenshot() integrado de Playwright basta para suites pequeñas: menos de unas cincuenta pantallas, sin librería de componentes compartida y sin necesidad de una UI de revisión de PR. Pásate a una herramienta específica como BackstopJS cuando necesites un flujo de aprobación de baselines en condiciones, un informe de revisión y renderizado normalizado por Docker, algo que gana importancia a medida que crece tu número de baselines.
¿Sigue manteniéndose Lost Pixel?
No. Lost Pixel se archivó en abril de 2026 y no es candidato para despliegues nuevos. Usa BackstopJS para un montaje de pruebas de regresión visual autoalojado.