Los binarios del navegador de Playwright dependen de unas dos docenas de bibliotecas del sistema, y las diferencias entre las versiones de esas bibliotecas en tu laptop y en CI causan fallos que no tienen nada que ver con tus tests. La imagen Docker oficial de Playwright en mcr.microsoft.com/playwright empaqueta los tres navegadores y sus dependencias en una sola imagen que corre de forma idéntica en cualquier lugar, siempre que el tag de la imagen coincida con la versión de @playwright/test en tu package.json. Esta guía cubre cómo escribir un Dockerfile, cómo montar los reportes de tests de vuelta al host, Docker Compose para testear contra un stack de aplicación local, y cómo integrar el contenedor en GitHub Actions.

Por qué Docker importa para QA

Los suites de tests de navegador son inusualmente sensibles a su entorno. Playwright instala binarios de Chromium, Firefox y WebKit que dependen de bibliotecas específicas del sistema: libglib, libnss, libatk, y alrededor de dos docenas más. En tu laptop probablemente están en la versión correcta porque instalaste Playwright recientemente. En la computadora de un compañero del 2022, puede que no. En un runner de CI configurado hace seis meses con una imagen de Ubuntu diferente, la incompatibilidad se convierte en un fallo de build a las 2am.

La solución tradicional es una página de wiki llamada "Configuración del entorno de desarrollo" que queda desactualizada en el momento en que alguien la escribe. La solución de Docker es un archivo en tu repositorio llamado Dockerfile que describe exactamente el entorno que necesitan tus tests. Cualquiera que ejecute docker build obtiene el mismo entorno, sin importar qué sistema operativo use o qué instaló la semana pasada.

Para QA específicamente, Docker trae tres beneficios concretos. Primero, paridad de entorno: los tests corren dentro del mismo contenedor localmente y en CI, así que un test que pasa localmente significa algo. Segundo, sin overhead de configuración: un nuevo integrante del equipo puede ejecutar todo el suite de tests con dos comandos sin instalar Node, Playwright ni ninguna dependencia de navegador. Tercero, aislamiento: las ejecuciones de tests no interfieren entre sí ni con lo demás que esté instalado en la máquina host.

Docker no reemplaza entender tus fallos de tests. Elimina la categoría de fallos causados por diferencias de entorno. Los fallos restantes son bugs reales, y eso es exactamente lo que quieres.

Conceptos básicos de Docker para testers

Si nunca trabajaste con Docker, el modelo mental es directo. Una imagen es una instantánea de un sistema de archivos: un sistema operativo, paquetes instalados, tu código y variables de entorno, todo congelado en un punto en el tiempo. Un contenedor es una instancia en ejecución de una imagen: piensa en la imagen como una clase y en el contenedor como un objeto.

Construyes una imagen a partir de un Dockerfile, un archivo de texto que lista los pasos para construirla. Ejecutas un contenedor desde una imagen con docker run. Cuando el contenedor termina, se detiene. Nada de lo que hizo cambia la imagen.

# Descargar una imagen existente de un registro
docker pull node:20-slim

# Ejecutar un contenedor desde esa imagen y ejecutar un comando
docker run node:20-slim node --version

# Construir una imagen desde un Dockerfile en el directorio actual
docker build -t my-playwright-tests .

# Ejecutar un contenedor desde la imagen que acabás de construir
docker run my-playwright-tests

El concepto clave para los flujos de trabajo de testing es que los contenedores son efímeros por defecto. Los archivos creados dentro de un contenedor desaparecen cuando se detiene, incluidos los reportes de tests. Esto se resuelve con volúmenes montados, que cubrimos en la sección sobre ejecución de tests.

Las imágenes se componen de capas. Cada instrucción en un Dockerfile crea una nueva capa, y Docker cachea las capas que no cambiaron. Esto es lo que hace que las reconstrucciones sean rápidas: si cambias el código de los tests pero no las dependencias, Docker reutiliza la capa cacheada que instaló esas dependencias y solo reconstruye las capas que cambiaron.

La imagen Docker oficial de Playwright

El equipo de Playwright publica y mantiene una imagen Docker oficial en mcr.microsoft.com/playwright. Esta imagen viene con todo lo que los tests de navegador necesitan: Node.js, el paquete de Playwright y los tres binarios de navegador (Chromium, Firefox, WebKit) con sus dependencias de bibliotecas del sistema preinstaladas.

# Descargar la imagen que coincide con tu versión de Playwright
docker pull mcr.microsoft.com/playwright:v1.52.0-jammy

# Ver qué tiene adentro
docker run --rm mcr.microsoft.com/playwright:v1.52.0-jammy node --version
docker run --rm mcr.microsoft.com/playwright:v1.52.0-jammy npx playwright --version

El formato del tag es v{versión-playwright}-{nombre-ubuntu}. jammy es Ubuntu 22.04, la opción más estable y ampliamente usada. También existe una variante noble para Ubuntu 24.04.

La imagen es grande (alrededor de 1.8GB) porque contiene tres instalaciones completas de navegador. Ese tamaño es el precio de tener todo preinstalado sin necesidad de ejecutar playwright install --with-deps al iniciar el contenedor. En entornos de CI donde descargás la imagen repetidamente, vale la pena porque el caché de capas de Docker significa que solo la descargás una vez por runner, a menos que cambie el tag.

La regla crítica: el tag de la imagen debe coincidir con tu versión de @playwright/test en package.json. Si usas v1.52.0-jammy pero tu proyecto tiene "@playwright/test": "1.50.0", las incompatibilidades de API causarán fallos confusos. Fijá los dos.

Escribir un Dockerfile para tu proyecto de Playwright

Empieza con la imagen oficial de Playwright como base. Acá hay un Dockerfile completo y listo para producción:

# Usar la imagen oficial de Playwright: fijar la versión para que coincida con package.json
FROM mcr.microsoft.com/playwright:v1.52.0-jammy

# Establecer el directorio de trabajo dentro del contenedor
WORKDIR /app

# Copiar los archivos de paquetes primero para mejor caché de capas
# Esta capa se reconstruye solo cuando cambian las dependencias
COPY package.json package-lock.json ./

# Instalar dependencias del proyecto (los navegadores ya están en la imagen base)
RUN npm ci

# Copiar el resto del proyecto
COPY . .

# Comando por defecto: ejecutar todos los tests
CMD ["npx", "playwright", "test"]

El orden de las instrucciones COPY y RUN importa para el caché. Al copiar package.json y package-lock.json primero y ejecutar npm ci antes de copiar el resto del código fuente, se garantiza que la capa de instalación de dependencias se cachee y reutilice cuando cambien los archivos de tests. Si copiás todo primero y después ejecutás npm ci, cualquier cambio en cualquier archivo invalida el caché de dependencias.

Notá que no hay un paso npx playwright install. La imagen base ya tiene los navegadores. Esto hace el build significativamente más rápido que una configuración que empieza desde una imagen de Node simple.

Agregá un archivo .dockerignore junto a tu Dockerfile para excluir archivos que no pertenecen a la imagen:

# .dockerignore
node_modules
playwright-report
test-results
.git
.github
*.md

Excluir node_modules es especialmente importante. Sin esto, Docker copiaría los node_modules locales dentro de la imagen antes de ejecutar npm ci, lo que desperdicia tiempo y puede introducir binarios específicos de plataforma que no funcionan dentro del contenedor Linux.

Construir la imagen:

docker build -t playwright-tests:local .

El flag -t etiqueta la imagen con un nombre. playwright-tests:local es una convención para distinguir imágenes construidas localmente de imágenes de registro.

Ejecutar tests en un contenedor

Una vez construida la imagen, ejecutar tests es un solo comando:

docker run --rm playwright-tests:local

--rm elimina automáticamente el contenedor cuando termina. Sin él, los contenedores detenidos se acumulan y consumen espacio en disco.

El problema es que los reportes de tests se escriben dentro del contenedor y desaparecen cuando se detiene. Corregilo con un volumen montado usando -v:

docker run --rm \
  -v $(pwd)/playwright-report:/app/playwright-report \
  -v $(pwd)/test-results:/app/test-results \
  playwright-tests:local

El flag -v mapea un directorio del host a un directorio del contenedor. Cuando Playwright escribe el reporte HTML en /app/playwright-report dentro del contenedor, en realidad escribe en $(pwd)/playwright-report en tu máquina host. Después de que el contenedor se detiene, el reporte está ahí, listo para abrir.

En Windows, reemplaza $(pwd) con %cd% en el Símbolo del sistema o ${PWD} en PowerShell:

docker run --rm `
  -v ${PWD}/playwright-report:/app/playwright-report `
  -v ${PWD}/test-results:/app/test-results `
  playwright-tests:local

Para ejecutar un subconjunto de tests, sobreescribe el CMD por defecto agregando argumentos:

# Ejecutar solo tests con un tag específico
docker run --rm playwright-tests:local npx playwright test --grep @smoke

# Ejecutar solo Chromium
docker run --rm playwright-tests:local npx playwright test --project=chromium

# Ejecutar un archivo de test específico
docker run --rm playwright-tests:local npx playwright test tests/login.spec.ts

Pasa variables de entorno con -e:

docker run --rm \
  -e BASE_URL=https://staging.example.com \
  -e TEST_USER=testuser \
  -e TEST_PASSWORD=secret \
  -v $(pwd)/playwright-report:/app/playwright-report \
  playwright-tests:local

Agrega un script en package.json para el comando completo docker run con los flags de volumen, para que el equipo no tenga que recordarlos. npm run test:docker es más fácil de documentar y más difícil de tipear mal que el comando completo.

docker-compose para ejecuciones locales de tests

Cuando los tests corren contra una aplicación, necesitás tanto la app como el contenedor de tests corriendo al mismo tiempo con acceso de red entre ellos. docker-compose gestiona configuraciones de múltiples contenedores con un solo archivo.

Acá hay un docker-compose.yml para una configuración típica donde los tests de Playwright corren contra una aplicación web local:

# docker-compose.yml
version: '3.8'

services:
  # Tu aplicación bajo prueba
  app:
    image: your-app:latest
    ports:
      - '3000:3000'
    environment:
      NODE_ENV: test
      DATABASE_URL: postgres://user:pass@db:5432/testdb
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:3000/health']
      interval: 5s
      timeout: 5s
      retries: 10

  # Base de datos (si la app la necesita)
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: testdb
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U user -d testdb']
      interval: 3s
      timeout: 3s
      retries: 15

  # Contenedor de tests de Playwright
  tests:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      BASE_URL: http://app:3000
    depends_on:
      app:
        condition: service_healthy
    volumes:
      - ./playwright-report:/app/playwright-report
      - ./test-results:/app/test-results
    # No iniciar los tests automáticamente: ejecutar bajo demanda
    profiles:
      - testing

El profiles: [testing] en el servicio de tests significa que no se inicia cuando ejecutas docker-compose up solo (que iniciaría la app y la base de datos). Para ejecutar los tests:

# Iniciar la app y la base de datos
docker-compose up -d app db

# Ejecutar los tests
docker-compose run --rm tests

# O iniciar todo y ejecutar los tests en un solo comando
docker-compose --profile testing run --rm tests

# Bajar todo
docker-compose down

Los servicios se comunican usando sus nombres de servicio como hostnames. El contenedor de tests configura BASE_URL: http://app:3000, y el DNS interno de Docker resuelve app a la IP del contenedor de la aplicación. Tu playwright.config.ts lee process.env.BASE_URL, así que localmente apunta a localhost:3000 y dentro del contenedor apunta a http://app:3000.

El depends_on con condition: service_healthy es importante. Sin él, Playwright arrancaría antes de que la app esté lista para aceptar conexiones y todos los tests fallarían con un error de conexión. El healthcheck consulta hasta que la app responde, luego Docker inicia el contenedor dependiente.

Integrar Docker en GitHub Actions

GitHub Actions puede usar un contenedor Docker como entorno de ejecución para un job completo, o podés ejecutar docker build y docker run como pasos. Para Playwright específicamente, usar el contenedor directamente como entorno del job es el enfoque más limpio.

# .github/workflows/playwright.yml
name: Playwright Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 60

    # Usar la imagen de Playwright como contenedor del job
    container:
      image: mcr.microsoft.com/playwright:v1.52.0-jammy
      options: --user 1001

    steps:
      - name: Checkout del código
        uses: actions/checkout@v4

      - name: Instalar dependencias
        run: npm ci

      - name: Ejecutar tests de Playwright
        run: npx playwright test
        env:
          BASE_URL: ${{ vars.BASE_URL }}
          TEST_USER: ${{ secrets.TEST_USER }}
          TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}

      - name: Subir reporte de tests
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 14

El bloque container: a nivel del job le dice a GitHub Actions que ejecute cada paso dentro del contenedor Docker especificado en lugar de directamente sobre el runner de Ubuntu. Los navegadores ya están instalados en la imagen, así que no hace falta el paso npx playwright install.

--user 1001 establece el usuario del contenedor para que coincida con el UID del runner de GitHub Actions. Sin esto, pueden ocurrir errores de permisos de archivos cuando Actions intenta escribir artefactos.

Para equipos que también quieren construir y subir su propia imagen de tests a un registro:

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout del código
        uses: actions/checkout@v4

      - name: Construir imagen de tests
        run: docker build -t playwright-tests:${{ github.sha }} .

      - name: Ejecutar tests
        run: |
          docker run --rm \
            -e BASE_URL=${{ vars.BASE_URL }} \
            -e TEST_USER=${{ secrets.TEST_USER }} \
            -e TEST_PASSWORD=${{ secrets.TEST_PASSWORD }} \
            -v ${{ github.workspace }}/playwright-report:/app/playwright-report \
            playwright-tests:${{ github.sha }}

      - name: Subir reporte de tests
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 14

Usar ${{ github.sha }} como tag de imagen vincula la imagen al commit exacto, lo que facilita correlacionar ejecuciones de tests con versiones de código si estás subiendo a un registro.

Problemas comunes y cómo resolverlos

Los contenedores Docker introducen un pequeño conjunto de problemas que aparecen regularmente al ejecutar Playwright.

Errores de permisos de archivos en los reportes. Cuando Playwright escribe en un directorio con volumen montado, los archivos son propiedad de cualquier usuario que corra el contenedor (a menudo root). Tu usuario del host no puede modificarlos ni eliminarlos sin sudo. La corrección es establecer el usuario del contenedor en tiempo de ejecución:

docker run --rm \
  --user $(id -u):$(id -g) \
  -v $(pwd)/playwright-report:/app/playwright-report \
  playwright-tests:local

Esto ejecuta el contenedor como tu usuario actual del host, así que los archivos del reporte son tuyos. En CI, usa --user 1001 como se muestra en el workflow anterior.

Modo headless y errores de pantalla. Playwright corre en modo headless por defecto, lo que funciona dentro de contenedores sin ningún cambio. Si estás ejecutando en modo con interfaz visible (para depuración) dentro de un contenedor, necesitarías una pantalla virtual. No hagas esto. Usa headless en contenedores y con interfaz visible localmente. Si un test se comporta diferente en headless vs. con interfaz visible, eso es un bug legítimo para investigar, no un problema de Docker. Builds lentos por caché de capas faltante. Si reconstruir la imagen tarda 3+ minutos cada vez, el caché no está funcionando. La causa más común es copiar todo el proyecto antes de instalar las dependencias:

# Lento: cualquier cambio de archivo invalida el caché de npm ci
COPY . .
RUN npm ci

# Rápido: solo los cambios en los archivos de paquetes invalidan el caché de npm ci
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

En CI, el caché de capas de Docker requiere configuración explícita. Agregá esto al workflow:

      - name: Configurar Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Construir imagen de tests
        uses: docker/build-push-action@v6
        with:
          context: .
          load: true
          tags: playwright-tests:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Las líneas cache-from: type=gha y cache-to: type=gha,mode=max usan el almacenamiento de caché de GitHub Actions para el caché de capas de Docker. Sin esto, cada ejecución de CI construye la imagen desde cero.

La imagen Docker oficial de Playwright incluye los tres navegadores, lo que la hace pesar alrededor de 1.8GB. En un runner de CI lento, la primera descarga puede tardar 3-4 minutos. Después de la descarga inicial, el caché de capas de Docker mantiene los inicios posteriores rápidos. Si tu registro tiene un espejo local configurado, úsalo.
Los tests no pueden alcanzar la aplicación. Dentro de un contenedor Docker, localhost se refiere al propio contenedor, no a tu máquina host. Si la app corre en tu host en localhost:3000, el contenedor no puede alcanzarla en esa dirección. Usá la IP del bridge de Docker de tu host (172.17.0.1 en Linux, host.docker.internal en Mac y Windows):

docker run --rm \
  -e BASE_URL=http://host.docker.internal:3000 \
  -v $(pwd)/playwright-report:/app/playwright-report \
  playwright-tests:local

Al usar docker-compose, este problema desaparece porque todos los servicios comparten una red y se comunican por nombre de servicio.

FAQ

¿Necesito Docker si ya uso GitHub Actions?

No necesariamente. Los runners de GitHub Actions funcionan bien para Playwright sin Docker: instalás Node, ejecutás playwright install --with-deps y listo. Docker se vuelve valioso cuando querés el mismo entorno localmente y en CI, cuando los tests corren contra un stack de múltiples servicios (app + base de datos), o cuando incorporás nuevos integrantes del equipo que necesitan una forma de ejecutar los tests sin configuración.

¿Debería construir mi propia imagen o usar la oficial de Playwright?

Empieza con la imagen oficial como base de tu FROM. Obtenés actualizaciones gratuitas a versiones del navegador cuando actualizás el tag, y la imagen la mantienen personas que conocen las dependencias de Playwright. Construí sobre ella solo cuando necesitás adiciones específicas del proyecto, como fuentes personalizadas, una versión específica de Node, o datos de test precargados.

¿Cómo depuro un test que falla solo en el contenedor?

Monta el directorio test-results y habilita la grabación de video en tu config de Playwright para tests fallidos. Después de la ejecución, la carpeta test-results/ en tu host contendrá videos y traces. Abre el trace con npx playwright show-trace test-results/.../trace.zip para ver exactamente qué ocurrió.

¿Puedo ejecutar tests en paralelo dentro de un contenedor?

Sí. La configuración workers de Playwright controla el paralelismo y funciona igual dentro de un contenedor. El límite práctico es la cantidad de CPUs del contenedor. Para suites grandes, ejecutá múltiples contenedores en paralelo (mediante sharding con matrix de GitHub Actions) en lugar de maximizar los workers dentro de un solo contenedor. Es más fácil de entender y de escalar.

Mi contenedor se construye bien pero los tests fallan con "el ejecutable no existe".

Esto generalmente significa que hay una incompatibilidad de versión entre el tag de la imagen y tu versión de @playwright/test. Verificá que mcr.microsoft.com/playwright:v1.52.0-jammy coincida con "@playwright/test": "1.52.0" en package.json. Si las versiones no coinciden, Playwright busca los navegadores en una ruta que no existe en la imagen.

¿Cómo paso un archivo .env al contenedor?

Usa --env-file con docker run:

docker run --rm \
  --env-file .env.test \
  -v $(pwd)/playwright-report:/app/playwright-report \
  playwright-tests:local

Nunca subas archivos .env al repositorio. Agrega .env* al .gitignore y usa secretos de CI para los valores sensibles.

→ See also: Docker para Ingenieros QA: Ejecutando Tests en Contenedores | GitHub Actions para Tests de Playwright: La Configuración Completa (2026) | Ejecución Paralela en Playwright: Workers, Fragmentos y Fragmentación para Mayor Velocidad