Volver al Centro de conocimiento
Despliegue

Automatización de navegador con Docker: despliegue y escalado

Despliega automatización de navegador en contenedores Docker con ejemplos de Dockerfile, escalado con Compose, volúmenes montados y buenas prácticas de producción.

BotBrowser Team

Documentación

Prefieres la documentación del producto mantenida?

Este artículo tiene una página equivalente en el centro de documentación. Usa los docs para el flujo canónico, las flags actuales y la referencia duradera.

Docker ofrece a un despliegue de navegador una imagen repetible, un sistema de archivos privado, su propio espacio de nombres de red y límites de recursos. Cada contenedor arranca desde el mismo estado conocido, una política de reinicio puede levantar un worker que se haya caído y un problema de memoria en un contenedor se queda dentro de ese contenedor. Son ventajas operativas reales para las flotas de BotBrowser, y por eso la mayoría de los equipos pasa de servidores montados a mano a imágenes.

TL;DR

  • Ajusta la memoria compartida antes que nada: un contenedor recibe 64 MB de /dev/shm por defecto, así que pasa --shm-size=2g o shm_size: 2gb antes de que las pestañas empiecen a caerse.
  • Arranca Xvfb en el entrypoint y exporta DISPLAY=:10.0 antes del navegador, o el contenedor sale con "Cannot open display".
  • Usa una base Ubuntu 22.04 o Debian: los binarios basados en Chrome necesitan glibc, y una imagen Alpine con musl no los arranca.
  • Pasa --init o init: true para que Docker recoja los procesos huérfanos del navegador, y da a cada contenedor su propio volumen de datos de usuario.
  • Publica el puerto de depuración como -p 127.0.0.1:9222:9222, nunca como un -p 9222:9222 sin dirección, porque quien alcanza ese puerto controla el navegador.

Contenido

Requisitos del contenedor antes del primer arranque

Docker no decide si un despliegue funciona frente a un sitio concreto, ni hace que todos los hosts se comporten igual. Aísla procesos y limita recursos. El navegador sigue necesitando las bibliotecas del sistema, la pantalla, la memoria compartida y la ruta de red que necesita cualquier navegador basado en Chrome. Acertar con esos cinco requisitos en la imagen es lo que separa un despliegue que arranca siempre de uno que falla al cabo de una hora.

La memoria compartida va primero. Chrome usa /dev/shm para la comunicación entre sus procesos, y un contenedor Docker recibe un /dev/shm de 64 MB salvo que lo cambies. Es muy poco para un navegador con páginas reales cargadas, y los síntomas habituales son pestañas que se cierran, páginas en blanco o un mensaje de falta de memoria mientras el host tiene RAM libre de sobra. Empieza con --shm-size=2g en docker run, o shm_size: 2gb en un archivo de Compose, y considera un riesgo cualquier valor por debajo de 1 GB. El tamaño que mantengas al final debe salir de medir tus propias páginas.

El segundo requisito es una pantalla virtual. BotBrowser necesita una pantalla X11 incluso en modo headless, así que el contenedor debe iniciar Xvfb antes que el navegador y exportar DISPLAY con el valor correspondiente. Si el entrypoint lanza primero el navegador, el contenedor termina enseguida con el mensaje "Cannot open display". Una breve espera tras iniciar Xvfb es la solución más simple, y una comprobación de salud que confirme que existe el socket de la pantalla es la más cuidadosa.

El tercero es la imagen base. Los navegadores basados en Chrome dependen de glibc, así que usa una base Debian o Ubuntu. Alpine usa musl y no ejecuta estos binarios. La imagen documentada usa Ubuntu 22.04 con las bibliotecas compartidas que aparecen en el Dockerfile de abajo. Una biblioteca ausente se manifiesta como un fallo de arranque, así que guarda la lista de paquetes en control de versiones y reconstruye la imagen en lugar de parchear un contenedor en marcha.

El cuarto es un proceso init. Un navegador crea procesos hijos y, cuando uno queda huérfano, nadie lo recoge. En una ejecución larga esos procesos zombi se acumulan. Pasa --init a docker run, o define init: true en Compose, para que el propio init de Docker sea el PID 1 y los limpie.

El quinto es un directorio de datos de usuario propio para cada proceso del navegador. Dentro de un contenedor ese directorio vive en la capa escribible del contenedor y desaparece cuando se elimina, así que monta un volumen en esa ruta si quieres que las cookies y el almacenamiento sobrevivan a un reinicio. No dejes que dos contenedores en ejecución compartan un directorio, porque dos navegadores que escriben en el mismo directorio de perfil se corrompen el estado mutuamente.

RequisitoAjuste de DockerQué ves si falta
Memoria compartida de al menos 1 a 2 GB--shm-size=2g o shm_size: 2gbCierres inesperados y páginas en blanco bajo carga
Pantalla virtual antes del arranqueXvfb en el entrypoint, DISPLAY=:10.0"Cannot open display" al iniciar
Imagen base con glibcDebian o Ubuntu, no AlpineEl binario no arranca
Proceso init--init o init: trueSe acumulan procesos zombi
Directorio de datos de usuario propioUn volumen por instancia en /app/dataSesiones perdidas, o estado corrupto si se comparte

Estructura de un contenedor Docker con BotBrowser: pantalla Xvfb, el navegador con un perfil montado, un puerto de depuración solo en loopback, límites de recursos, un volumen de perfiles de solo lectura y un volumen de datos de usuario por instancia.

Los binarios de Ubuntu y Linux requieren un plan de BotBrowser apto. La documentación de despliegue con Docker indica ENT Tier 1 o superior para ellos, así que confirma tu plan antes de construir una imagen alrededor del paquete .deb.

Construye la imagen y arranca un contenedor

El Dockerfile de abajo sigue la documentación de Docker de BotBrowser. Instala las bibliotecas compartidas y Xvfb, instala el paquete .deb de BotBrowser que descargaste para tu plan y crea los dos directorios que el contenedor usa para perfiles y datos de usuario. La opción --platform=linux/amd64 fija la arquitectura para que una imagen construida en un host se construya igual en otro.

FROM --platform=linux/amd64 ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
ENV DISPLAY=:10.0
RUN apt-get update && apt-get install -y \
    libnss3 libatk1.0-0 libatk-bridge2.0-0 \
    libcups2 libdrm2 libxcomposite1 libxdamage1 \
    libxrandr2 libgbm1 libxkbcommon0 libgtk-3-0 \
    libpangocairo-1.0-0 libpango-1.0-0 \
    libwayland-client0 libwayland-server0 \
    libdbus-1-3 libatspi2.0-0 libasound2 libxss1 \
    fonts-liberation libcurl4 libvulkan1 \
    xvfb curl ca-certificates \
    && rm -rf /var/lib/apt/lists/*
COPY botbrowser_*.deb /tmp/
RUN dpkg -i /tmp/botbrowser_*.deb || apt-get install -f -y \
    && rm /tmp/botbrowser_*.deb
RUN mkdir -p /app/profiles /app/data
WORKDIR /app
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
ENTRYPOINT ["/app/entrypoint.sh"]

El entrypoint inicia la pantalla virtual y luego se sustituye por el navegador mediante exec, de modo que las señales de docker stop llegan directamente al navegador. Todo lo que pases después del nombre de la imagen llega como "$@", así que la misma imagen puede ejecutarse con distintos perfiles y proxies sin reconstruirla.

#!/bin/bash
Xvfb :10 -screen 0 1920x1080x24 -ac &
sleep 1
exec chromium-browser \
    --headless \
    --no-sandbox \
    --disable-dev-shm-usage \
    --user-data-dir=/app/data \
    --remote-debugging-port=9222 \
    --remote-debugging-address=0.0.0.0 \
    "$@"

Dos opciones de ese script merecen un comentario. --disable-dev-shm-usage indica a Chrome que escriba sus archivos de memoria compartida en /tmp en lugar de /dev/shm. Es una mitigación alternativa frente a un /dev/shm pequeño. Una vez activada, Chrome deja de usar /dev/shm, así que --shm-size importa sobre todo cuando la opción no está, y mantener ambas es una precaución. --no-sandbox se usa porque los contenedores suelen ejecutarse sin los privilegios que pide el sandbox de Chrome. Esa decisión traslada el límite de aislamiento al contenedor, así que ejecuta el contenedor con un usuario limitado cuando tu imagen lo permita, mantén los límites de recursos de Docker y no montes rutas del host que no entregarías al navegador.

Construye la imagen y arranca un único contenedor con el tamaño de memoria compartida, el proceso init, un montaje de perfiles de solo lectura, un volumen de datos de usuario y el puerto de depuración enlazado a la dirección de loopback del host.

docker build -t botbrowser:latest .
docker run -d --name botbrowser --init --shm-size=2g \
  -v "$PWD/profiles:/app/profiles:ro" \
  -v bb-data:/app/data \
  -p 127.0.0.1:9222:9222 \
  botbrowser:latest \
  --bot-profile=/app/profiles/profile.enc \
  --proxy-server=socks5://user:[email protected]:1080

La forma -p 127.0.0.1:9222:9222 importa. Un simple -p 9222:9222 publica el puerto en todas las interfaces del host, y el puerto de depuración remota da control completo del navegador a quien llegue a él. Enlazarlo a loopback lo deja accesible solo desde el host, y un túnel SSH o una red interna es la vía para llegar desde otro sitio. La referencia de docker run describe con detalle la sintaxis de publicación, la opción --shm-size y --init.

Detener un contenedor también forma parte del diseño. docker stop envía SIGTERM y espera diez segundos antes de enviar SIGKILL. Como el entrypoint cede el control al navegador con exec, el navegador recibe la señal y puede cerrar sus páginas y volcar los datos de usuario. Si tus sesiones necesitan más tiempo para terminar, aumenta el plazo con --stop-timeout o stop_grace_period en Compose, y etiqueta tus imágenes con una versión en lugar de latest para que un retroceso sea un simple cambio de etiqueta.

Las credenciales del proxy en el ejemplo son marcadores de ejemplo. Pasa los valores reales en tiempo de ejecución mediante tu almacén de secretos, un archivo de entorno o los secretos de Compose, y mantenlos fuera de la imagen y del control de versiones.

Monta perfiles y conserva los datos de usuario por instancia

BotBrowser carga su configuración desde un archivo de perfil. En un contenedor, la forma limpia de entregar ese archivo es un montaje de solo lectura, como en el comando anterior. La imagen sigue siendo la misma, el perfil vive en el host y sustituir un perfil significa editar un archivo y reiniciar el contenedor. Nadie reconstruye nada, y nadie puede modificar el archivo desde dentro del contenedor porque el montaje es de solo lectura.

Usa --bot-profile cuando cada contenedor deba arrancar desde un perfil concreto. Es la forma adecuada para una flota en la que un contenedor tiene un proxy, una región y un perfil elegidos a propósito. La ruta que pases debe coincidir con la ruta dentro del contenedor, y una discrepancia entre el destino del montaje y la opción es el motivo más común de un error de arranque por perfil no encontrado.

Usa --bot-profile-dir cuando un contenedor deba elegir por sí mismo un perfil de un directorio. BotBrowser selecciona un archivo .enc del directorio cada vez que arranca. Las dos opciones no se pueden combinar, y si pasas ambas, el directorio tiene prioridad. Elige la forma de directorio cuando la variedad de perfiles importe más que una pareja fija, y la de archivo único cuando el perfil de un contenedor deba conocerse de antemano.

Haz coincidir el paquete de perfiles con la versión mayor de BotBrowser. Emparejar un paquete de perfiles con otra versión mayor no es lo que describe la documentación, así que mantén un directorio de perfiles por versión y monta el que corresponda a la imagen. Como los archivos de perfil se montan y no se incorporan a la imagen, actualizar el navegador y actualizar los perfiles son dos pasos distintos y reversibles.

El archivo de perfil montado, y no el host del contenedor, controla el comportamiento definido por el perfil. Es una propiedad útil para operar, pero no debe leerse como una promesa de que cualquier imagen base, kernel, host, proxy y sitio de destino produzca el mismo resultado. Prueba la combinación que realmente ejecutas y guarda en tu registro de despliegue la etiqueta de la imagen, la versión de BotBrowser y el nombre del archivo de perfil.

Los datos de usuario merecen el mismo cuidado. Un volumen con nombre por instancia, como bb-data o bb1-data en los ejemplos, conserva cookies, almacenamiento local y otro estado de sesión entre reinicios y mantiene cada instancia independiente. Elimina el volumen solo cuando quieras que esa instancia arranque limpia. Para ejecuciones cortas y desechables, omite el volumen y deja que la capa escribible del contenedor desaparezca con él.

Escala con Docker Compose

Compose describe cada instancia como un servicio y reúne puertos, montajes, límites y comportamiento de reinicio en un único archivo revisable. La versión de abajo ejecuta dos contenedores desde la misma imagen. Cada uno tiene su propio archivo de perfil, su propio proxy, su propio volumen de datos de usuario y su propio puerto de depuración en el host.

services:
  bb-1:
    image: botbrowser:latest
    init: true
    shm_size: 2gb
    mem_limit: 4g
    cpus: 2
    ports:
      - '127.0.0.1:9222:9222'
    volumes:
      - ./profiles:/app/profiles:ro
      - bb1-data:/app/data
    command:
      - --bot-profile=/app/profiles/profile-1.enc
      - --proxy-server=socks5://user:[email protected]:1080
    restart: unless-stopped
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:9222/json/version']
      interval: 30s
      timeout: 10s
      retries: 3
  bb-2:
    image: botbrowser:latest
    init: true
    shm_size: 2gb
    mem_limit: 4g
    cpus: 2
    ports:
      - '127.0.0.1:9223:9222'
    volumes:
      - ./profiles:/app/profiles:ro
      - bb2-data:/app/data
    command:
      - --bot-profile=/app/profiles/profile-2.enc
      - --proxy-server=socks5://user:[email protected]:1080
    restart: unless-stopped
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:9222/json/version']
      interval: 30s
      timeout: 10s
      retries: 3
volumes:
  bb1-data:
  bb2-data:

Los límites de memoria y CPU evitan que un contenedor descontrolado consuma todo el host, y la comprobación de salud llama al endpoint de depuración dentro del contenedor para que Docker pueda marcar como no saludable una instancia que no responde. La política restart: unless-stopped recupera una instancia caída sin que nadie la vigile. El controlador de registros predeterminado de Docker conserva los registros del contenedor sin límite de tamaño, así que define las opciones de registro max-size y max-file en los servicios de larga duración antes de que una instancia muy activa llene el disco. Unos límites demasiado ajustados provocarán los mismos cierres que un /dev/shm pequeño, así que dimensiónalos con mediciones de tus propias páginas y espera que las cifras cambien entre cargas de trabajo.

Las asignaciones de puerto fijas sirven para unos pocos servicios con nombre y encajan mal con las réplicas. Cuando quieras varios contenedores idénticos que elijan perfil por su cuenta, elimina el puerto fijo del host y deja que Docker elija uno.

services:
  botbrowser:
    image: botbrowser:latest
    init: true
    shm_size: 2gb
    ports:
      - '127.0.0.1::9222'
    volumes:
      - ./profiles:/app/profiles:ro
    command:
      - --bot-profile-dir=/app/profiles
    restart: unless-stopped

La forma 127.0.0.1::9222 publica en loopback un puerto libre aleatorio del host para cada réplica. Arranca las réplicas y lista los puertos asignados con los comandos de abajo. Cada réplica tiene su propio sistema de archivos de contenedor, así que sus datos de usuario son privados y desaparecen con ella. Si una réplica necesita estado de sesión duradero, dale un servicio con nombre y un volumen con nombre, como en el primer archivo.

docker compose up -d --scale botbrowser=3
docker compose ps

La capacidad depende del host, de las páginas que cargues y de los proxies que uses. Aumenta el número de réplicas por pasos, vigila memoria, CPU y reinicios en el host, y detente cuando el margen sea escaso. La guía de optimización del rendimiento trata la medición, y el artículo sobre la configuración de un servidor headless describe los mismos requisitos de pantalla y bibliotecas en un host sin contenedores, lo que ayuda al comparar un contenedor con un servidor que ya funciona.

Verifica el contenedor y lee los fallos habituales

Un despliegue está listo cuando el navegador arranca, el endpoint de depuración responde, una navegación termina y los registros no contienen errores del cargador. Empieza por los registros y el endpoint.

docker logs botbrowser
curl http://127.0.0.1:9222/json/version

Una biblioteca compartida ausente aparece en el registro del contenedor al arrancar como un error del cargador que nombra el archivo, y un contenedor que termina de inmediato casi siempre se debe a esto, a una ruta de perfil incorrecta o a que falta la pantalla. Si el endpoint devuelve un documento JSON con la versión del navegador, el proceso está en marcha y escuchando. Después ejecuta una navegación de prueba desde el host. El fragmento de abajo se conecta por el puerto de loopback y carga una página neutra.

import { chromium } from 'playwright-core';
const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
const context = browser.contexts()[0];
const page = await context.newPage();
await page.goto('https://example.com');
console.log(await page.title());
await browser.close();

Imprimir el título de la página basta. Demuestra que la pantalla, las bibliotecas, el montaje del perfil y la ruta de red funcionan juntos, y te da un comando que repetir tras cada cambio de imagen. Mantén esta comprobación pequeña. Una prueba de despliegue debe confirmar que el contenedor funciona, y no debe intentar medir el comportamiento de ningún sitio de terceros. Para una comprobación visual desde dentro de un contenedor, Capturas fiables en navegadores headless explica cómo hacer capturas de forma consistente.

ProblemaQué comprobar
El contenedor termina de inmediatoEjecuta docker logs y busca una biblioteca ausente, una ruta de perfil ilegible o una pantalla que falta
"Cannot open display"Inicia Xvfb antes que el navegador y confirma que DISPLAY=:10.0 está definido en la imagen
"Out of memory" o páginas que se cierranAumenta --shm-size y, si el propio contenedor tiene tope, aumenta el límite de memoria
No se llega al puerto 9222 desde el hostMantén --remote-debugging-address=0.0.0.0 dentro del contenedor y publica el puerto en loopback
Se acumulan procesos zombiAñade --init o init: true
Perfil no encontradoCompara el destino del montaje con la ruta de --bot-profile y usa docker exec para listar el directorio
Primer arranque lentoEl primer arranque con un directorio de datos de usuario vacío tarda más, y los siguientes lo reutilizan

Trata el puerto de depuración como un riesgo de producción. Quien pueda conectarse puede leer páginas, ejecutar scripts y controlar el navegador, así que nunca lo publiques en una interfaz pública, coloca delante una regla de firewall o una red privada y da a cada contenedor su propio puerto. Los registros son el otro lugar donde hay que tener cuidado. Elimina las credenciales del proxy y los identificadores de cuenta antes de compartir un registro fuera del equipo.

Qué cubre BotBrowser y qué sigue siendo tuyo

BotBrowser permite cargar un archivo de perfil desde un volumen montado en solo lectura con --bot-profile, o elegir uno por contenedor con --bot-profile-dir, de modo que cada contenedor arranca con la configuración definida por su perfil montado y los perfiles pueden cambiar sin reconstruir la imagen. BotBrowser no puede proporcionar la memoria compartida, la pantalla virtual, las bibliotecas del sistema, la calidad del proxy ni la orquestación del contenedor, y no garantiza que ningún sitio de destino acepte el tráfico ni que haya binarios de Linux disponibles sin un plan apto.

El reparto de abajo mantiene claras las responsabilidades cuando algo falla. La primera columna es lo que hace el navegador con un archivo montado. La segunda es la parte que opera tu equipo.

Lo cubre BotBrowserLo opera tu equipo
Cargar un archivo de perfil pasado con --bot-profileMemoria compartida, límites de CPU y memoria, y el host
Elegir un perfil de --bot-profile-dir al arrancarLa pantalla virtual, las bibliotecas del sistema y la imagen base
Aplicar la configuración de proxy dada con --proxy-serverCalidad del proxy, rotación y la ruta de red
Ejecutarse con el directorio de datos de usuario que indiquesVolúmenes, copias de seguridad y limpieza del estado de sesión
Arrancar desde el perfil montado, sin reconstruir la imagenExposición del puerto de depuración, política de red, control de acceso y orquestación

Usa esa tabla como lista de comprobación durante un incidente. Un fallo que menciona la memoria es un límite del contenedor. Un error de arranque que nombra una biblioteca es un problema de imagen. Un error de ruta de perfil es un problema de montaje. Ninguno es una cuestión del perfil del navegador, y ninguno se resuelve cambiando el perfil. Deja los resultados en sitios concretos fuera de la definición de terminado del despliegue, porque dependen de las reglas del propio sitio y del tráfico que estés autorizado a enviar.

Cuando estés listo para construir, descarga BotBrowser y comprueba que tu plan incluye binarios de Linux. Para una flota mayor, la página de empresas describe las opciones de plan. Las recetas de CLI recogen las opciones usadas aquí y sus combinaciones, lo que resulta útil al ampliar el entrypoint.

Fuentes

Conclusión práctica

Escribe las cinco opciones del contenedor en la imagen y en la orden de arranque, no en un manual que nadie consulta: el valor de --shm-size, el paso de Xvfb con DISPLAY=:10.0, la base Ubuntu 22.04, --init y un volumen de datos de usuario por instancia. Después arranca un contenedor, monta el perfil con --bot-profile y confirma que el puerto 9222 publicado en loopback responde antes de escalar el mismo archivo con Compose.

#Docker#despliegue#servidor#automatización#Devops

Este artículo forma parte de la guía: Despliegue y versión del navegador: mapa operativo

Lleva BotBrowser de la investigación a producción

Usa estas guías para entender el modelo y después avanzar hacia validación multiplataforma, contextos aislados y despliegue de navegador preparado para escalar.