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
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/shmpor defecto, así que pasa--shm-size=2goshm_size: 2gbantes de que las pestañas empiecen a caerse. - Arranca Xvfb en el entrypoint y exporta
DISPLAY=:10.0antes 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
--initoinit: truepara 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:9222sin dirección, porque quien alcanza ese puerto controla el navegador.
Contenido
- Requisitos del contenedor antes del primer arranque
- Construye la imagen y arranca un contenedor
- Monta perfiles y conserva los datos de usuario por instancia
- Escala con Docker Compose
- Verifica el contenedor y lee los fallos habituales
- Qué cubre BotBrowser y qué sigue siendo tuyo
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.
| Requisito | Ajuste de Docker | Qué ves si falta |
|---|---|---|
| Memoria compartida de al menos 1 a 2 GB | --shm-size=2g o shm_size: 2gb | Cierres inesperados y páginas en blanco bajo carga |
| Pantalla virtual antes del arranque | Xvfb en el entrypoint, DISPLAY=:10.0 | "Cannot open display" al iniciar |
| Imagen base con glibc | Debian o Ubuntu, no Alpine | El binario no arranca |
| Proceso init | --init o init: true | Se acumulan procesos zombi |
| Directorio de datos de usuario propio | Un volumen por instancia en /app/data | Sesiones perdidas, o estado corrupto si se comparte |
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.
| Problema | Qué comprobar |
|---|---|
| El contenedor termina de inmediato | Ejecuta 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 cierran | Aumenta --shm-size y, si el propio contenedor tiene tope, aumenta el límite de memoria |
| No se llega al puerto 9222 desde el host | Mantén --remote-debugging-address=0.0.0.0 dentro del contenedor y publica el puerto en loopback |
| Se acumulan procesos zombi | Añade --init o init: true |
| Perfil no encontrado | Compara el destino del montaje con la ruta de --bot-profile y usa docker exec para listar el directorio |
| Primer arranque lento | El 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 BotBrowser | Lo opera tu equipo |
|---|---|
Cargar un archivo de perfil pasado con --bot-profile | Memoria compartida, límites de CPU y memoria, y el host |
Elegir un perfil de --bot-profile-dir al arrancar | La pantalla virtual, las bibliotecas del sistema y la imagen base |
Aplicar la configuración de proxy dada con --proxy-server | Calidad del proxy, rotación y la ruta de red |
| Ejecutarse con el directorio de datos de usuario que indiques | Volúmenes, copias de seguridad y limpieza del estado de sesión |
| Arrancar desde el perfil montado, sin reconstruir la imagen | Exposició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
- Documentación de despliegue con Docker de BotBrowser
- Referencia de docker container run
- Referencia de servicios de Docker Compose
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.
Este artículo forma parte de la guía: Despliegue y versión del navegador: mapa operativo
Artículos Relacionados
Escalar contextos con perfiles Per-Context
DespliegueLímites de recursos para contenedores: método práctico
DespliegueBotBrowser headless en servidores Ubuntu
DespliegueRendimiento del navegador: optimización a escala
DespliegueAislamiento de procesos del navegador y responsabilidad operativa
DespliegueCapturas fiables en navegadores headless
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.