Docker 浏览器自动化:部署与扩展指南
在 Docker 容器中部署浏览器自动化,包含 Dockerfile 示例、Compose 扩展、卷挂载和生产环境最佳实践。
BotBrowser Team
Docker 为浏览器部署提供可重复的镜像、独立的文件系统、专属的网络命名空间和资源限制。每个容器都从同一个已知状态启动,崩溃的 worker 可以由重启策略拉起,一个容器里的内存问题也只留在该容器内部。这些都是 BotBrowser 集群真实的运维收益,也是多数团队从手工搭建的服务器转向镜像的原因。
TL;DR
- 先解决共享内存:容器默认只有 64 MB 的
/dev/shm,在页面开始崩溃前设置--shm-size=2g或shm_size: 2gb。 - 在 entrypoint 里先启动 Xvfb 并导出
DISPLAY=:10.0,否则容器会直接以 “Cannot open display” 退出。 - 使用 Ubuntu 22.04 或 Debian 基础镜像:基于 Chrome 的二进制依赖 glibc,用 musl 的 Alpine 无法启动。
- 传入
--init或init: true,让 Docker 回收孤立的浏览器进程,并为每个容器准备独立的用户数据卷。 - 调试端口写成
-p 127.0.0.1:9222:9222,不要用裸的-p 9222:9222,因为能连上该端口的人就能完全控制浏览器。
目录
- 首次启动前的容器要求
- 构建镜像并启动一个容器
- 挂载 Profile,并为每个实例保留用户数据
- 使用 Docker Compose 扩展
- 验证容器并排查常见故障
- BotBrowser 负责什么,什么仍由你负责
首次启动前的容器要求
Docker 并不决定部署能否在某个具体网站上成功,也不会让所有宿主机表现一致。它隔离进程并限制资源。浏览器仍然需要系统库、显示、共享内存和网络路由,这与任何基于 Chrome 的浏览器相同。在镜像里把这五项要求处理好,是区分“每次都能启动”和“运行一小时后出问题”的关键。
共享内存排在第一位。Chrome 通过 /dev/shm 在自身的多个进程之间通信,而 Docker 容器默认只有 64 MB 的 /dev/shm,除非你修改它。对加载真实页面的浏览器来说这远远不够,常见症状是标签页崩溃、页面空白,或者宿主机明明有大量空闲内存却提示内存不足。先在 docker run 中使用 --shm-size=2g,或在 Compose 文件中使用 shm_size: 2gb,并把低于 1 GB 的取值视为风险。最终保留的大小应来自对你自己页面的实测。
第二项要求是虚拟显示。即使在 headless 模式下,BotBrowser 也需要 X11 显示,所以容器必须先启动 Xvfb,再启动浏览器,并导出匹配的 DISPLAY。如果 entrypoint 先启动了浏览器,容器会立刻退出并提示 "Cannot open display"。在 Xvfb 启动后短暂等待是最简单的修复,用健康检查确认显示套接字存在则更稳妥。
第三项是基础镜像。基于 Chrome 的浏览器依赖 glibc,所以请使用 Debian 或 Ubuntu 基础镜像。Alpine 使用 musl,无法运行这些二进制文件。文档中的镜像使用 Ubuntu 22.04,并安装了下方 Dockerfile 中列出的共享库。缺少库会表现为启动失败,所以请把软件包列表放进版本控制,并通过重新构建镜像修复,而不是去修补正在运行的容器。
第四项是 init 进程。浏览器会产生子进程,当其中一个成为孤儿进程时,没有人去回收它。长时间运行后,这些僵尸进程会不断累积。给 docker run 加上 --init,或在 Compose 中设置 init: true,让 Docker 自带的 init 成为 PID 1 并负责清理。
第五项是每个浏览器进程都有独立的用户数据目录。在容器内,这个目录位于容器的可写层,容器被删除时随之消失,所以如果希望 Cookie 和存储在重启后保留,就在该路径上挂载一个卷。不要让两个正在运行的容器共用同一个目录,因为两个浏览器写入同一个 Profile 目录会互相破坏状态。
| 要求 | Docker 设置 | 缺少时的表现 |
|---|---|---|
| 至少 1 到 2 GB 共享内存 | --shm-size=2g 或 shm_size: 2gb | 负载下崩溃和页面空白 |
| 启动前准备虚拟显示 | entrypoint 中运行 Xvfb,设置 DISPLAY=:10.0 | 启动时提示 "Cannot open display" |
| 基于 glibc 的基础镜像 | Debian 或 Ubuntu,不用 Alpine | 二进制文件无法启动 |
| init 进程 | --init 或 init: true | 僵尸进程不断累积 |
| 独立的用户数据目录 | 每个实例一个挂载到 /app/data 的卷 | 会话丢失,共用时状态损坏 |
Ubuntu 和 Linux 二进制文件需要符合条件的 BotBrowser 套餐。Docker 部署文档说明这些二进制文件需要 ENT Tier 1 或更高级别,所以在围绕 .deb 软件包构建镜像之前,请先确认你的套餐。
构建镜像并启动一个容器
下面的 Dockerfile 遵循 BotBrowser 的 Docker 文档。它安装共享库和 Xvfb,安装你按套餐下载的 BotBrowser .deb 软件包,并创建容器用于存放 Profile 和用户数据的两个目录。--platform=linux/amd64 选项固定了架构,使在一台宿主机上构建的镜像在另一台上也以同样方式构建。
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"]
entrypoint 先启动虚拟显示,再通过 exec 用浏览器替换自身,这样 docker stop 发出的信号会直接到达浏览器。镜像名称之后传入的所有参数都以 "$@" 的形式到达,所以同一个镜像可以使用不同的 Profile 和代理运行,无需重新构建。
#!/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 \
"$@"
脚本中有两个选项值得说明。--disable-dev-shm-usage 让 Chrome 把共享内存文件写到 /tmp 而不是 /dev/shm。它是应对过小 /dev/shm 的一种替代缓解措施。设置后 Chrome 不再使用 /dev/shm,因此 --shm-size 主要在没有该选项时才起作用,两者同时保留属于预防措施。使用 --no-sandbox 是因为容器通常在没有 Chrome 沙箱所需权限的情况下运行。这个选择把隔离边界移到了容器上,所以如果镜像允许,请用受限用户运行容器,保留 Docker 的资源限制,并且不要挂载你不会交给浏览器的宿主机路径。
构建镜像,然后启动单个容器,带上共享内存大小、init 进程、只读的 Profile 挂载、用户数据卷,以及绑定到宿主机回环地址的调试端口。
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
-p 127.0.0.1:9222:9222 这种写法很重要。只写 -p 9222:9222 会把端口发布在宿主机的所有网络接口上,而远程调试端口会让任何能连上的人完全控制浏览器。绑定到回环地址后,它只能从宿主机访问,需要从别处访问时可以使用 SSH 隧道或内部网络。docker run 参考文档详细说明了端口发布语法、--shm-size 选项和 --init。
停止容器也是设计的一部分。docker stop 会先发送 SIGTERM,等待十秒后再发送 SIGKILL。由于 entrypoint 通过 exec 把控制权交给浏览器,浏览器能收到信号,关闭页面并写出用户数据。如果你的会话需要更长时间才能结束,可以用 --stop-timeout 或 Compose 中的 stop_grace_period 延长等待时间,并且用版本号而不是 latest 给镜像打标签,这样回滚只需改一个标签。
示例中的代理凭据只是示意值。请在运行时通过你的密钥存储、环境文件或 Compose secrets 传入真实值,并让它们远离镜像和版本控制。
挂载 Profile,并为每个实例保留用户数据
BotBrowser 从 Profile 文件加载配置。在容器中,提供该文件的干净做法是只读绑定挂载,如上面的命令所示。镜像保持不变,Profile 放在宿主机上,更换 Profile 只需编辑文件并重启容器。无需重新构建任何东西,由于挂载是只读的,容器内部也无法修改该文件。
当每个容器应从一个指定的 Profile 启动时,使用 --bot-profile。这适合一个容器对应一个代理、一个地区和一个经过有意选择的 Profile 的集群。传入的路径必须与容器内的路径一致,挂载目标与该选项不一致是出现“找不到 Profile”启动错误的最常见原因。
当容器应该自行从目录中选择一个 Profile 时,使用 --bot-profile-dir。BotBrowser 每次启动都会从该目录中选取一个 .enc 文件。这两个选项不能同时使用,如果同时传入,目录优先。当 Profile 的多样性比固定搭配更重要时选择目录形式,当必须提前确定容器所用 Profile 时选择单文件形式。
让 Profile 包与 BotBrowser 的主版本保持一致。把 Profile 包与不同主版本搭配并不是文档描述的用法,所以为每个版本保留一个 Profile 目录,并挂载与镜像对应的那一个。由于 Profile 文件是挂载进来而不是打进镜像,升级浏览器和升级 Profile 就成为两个相互独立、可以回退的步骤。
决定由 Profile 定义的行为的,是挂载进来的 Profile 文件,而不是容器宿主机。这对运维很有用,但不要把它理解为任何基础镜像、内核、宿主机、代理和目标网站都会产生相同结果的承诺。请测试你实际运行的组合,并在部署记录中保存镜像标签、BotBrowser 版本和 Profile 文件名。
用户数据同样需要用心对待。为每个实例使用一个具名卷,例如示例中的 bb-data 或 bb1-data,可以在重启之间保留 Cookie、本地存储和其他会话状态,同时让各实例保持独立。只有想让该实例从空白状态开始时,才删除卷。对于短暂的一次性运行,可以完全不用卷,让容器的可写层随容器一起消失。
使用 Docker Compose 扩展
Compose 把每个实例描述为一个服务,并把端口、挂载、限制和重启行为放在一个便于审阅的文件里。下面的版本从同一个镜像运行两个容器。每个容器都有自己的 Profile 文件、自己的代理、自己的用户数据卷,以及宿主机上自己的调试端口。
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:
内存和 CPU 限制能防止某个失控的容器占满整台宿主机,健康检查会在容器内调用调试端点,使 Docker 能把无响应的实例标记为不健康。restart: unless-stopped 策略无需人工值守即可拉起崩溃的实例。Docker 默认的日志驱动不限制容器日志大小,所以请在长期运行的服务上设置 max-size 和 max-file 日志选项,避免繁忙的实例写满磁盘。限制过紧会造成与过小的 /dev/shm 相同的崩溃,所以请根据你自己页面的实测来确定大小,并预期不同工作负载的数值会有差异。
固定端口映射适合少数具名服务,不适合副本。当你想要多个相同的容器并让每个容器自行选择 Profile 时,去掉固定的宿主机端口,让 Docker 来选择。
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
127.0.0.1::9222 这种写法会为每个副本在回环地址上发布一个随机的空闲宿主机端口。启动副本,并用下面的命令列出分配到的端口。每个副本都有自己的容器文件系统,所以它的用户数据是私有的,并随它一起消失。如果某个副本需要持久的会话状态,请像第一个文件那样给它一个具名服务和一个具名卷。
docker compose up -d --scale botbrowser=3
docker compose ps
容量取决于宿主机、你加载的页面和你使用的代理。分步增加副本数量,观察宿主机的内存、CPU 和重启次数,余量变小时就停下。性能优化指南介绍了度量方法,无头服务器配置一文描述了在没有容器的宿主机上相同的显示和库要求,便于你把容器与已经正常工作的服务器做对比。
验证容器并排查常见故障
当浏览器启动、调试端点有响应、一次导航完成,并且日志中没有加载器错误时,部署就准备好了。先看日志和端点。
docker logs botbrowser
curl http://127.0.0.1:9222/json/version
缺少共享库时,容器日志在启动阶段会出现一条带有文件名的加载器错误,而立即退出的容器几乎总是由此、错误的 Profile 路径或缺少显示导致的。如果端点返回包含浏览器版本的 JSON 文档,说明进程正在运行并在监听。然后从宿主机执行一次测试导航。下面的片段通过回环端口连接,并加载一个中性页面。
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();
打印页面标题就足够了。它证明显示、库、Profile 挂载和网络路由能够一起工作,也为你提供了每次更换镜像后都可以重复的一条命令。让这项检查保持精简。部署测试应当确认容器能够工作,不应试图度量任何第三方网站的行为。如果想在容器内做可视化检查,可参阅 Headless 浏览器可靠截图工作流了解如何稳定地截图。
| 问题 | 检查什么 |
|---|---|
| 容器立即退出 | 运行 docker logs,查找缺失的库、无法读取的 Profile 路径或缺少的显示 |
| "Cannot open display" | 在浏览器之前启动 Xvfb,并确认镜像中设置了 DISPLAY=:10.0 |
| "Out of memory" 或页面崩溃 | 提高 --shm-size,如果容器本身有上限,再提高内存限制 |
| 无法从宿主机访问 9222 端口 | 在容器内保留 --remote-debugging-address=0.0.0.0,并把端口发布在回环地址上 |
| 僵尸进程不断累积 | 添加 --init 或 init: true |
| 找不到 Profile | 对比挂载目标与 --bot-profile 的路径,并用 docker exec 列出该目录 |
| 首次启动较慢 | 用户数据目录为空时首次启动耗时更长,之后的启动会复用它 |
请把调试端口当作生产风险。任何能连接的人都可以读取页面、运行脚本并控制浏览器,所以不要把它发布在公共接口上,在它前面设置防火墙规则或私有网络,并让每个容器使用自己的端口。日志是另一个需要小心的地方。在把日志分享到团队之外之前,先去掉代理凭据和账号标识。
BotBrowser 负责什么,什么仍由你负责
BotBrowser 支持通过 --bot-profile 从只读挂载卷加载 Profile 文件,或通过 --bot-profile-dir 为每个容器选取一个,因此每个容器都从其挂载的 Profile 所定义的配置启动,并且 Profile 可以在不重新构建镜像的情况下更换。BotBrowser 不能提供容器的共享内存、虚拟显示、系统库、代理质量或编排,也不保证任何目标网站会接受这些流量,或在没有符合条件的套餐时提供 Linux 二进制文件。
下面的划分能在出问题时让责任保持清晰。第一列是浏览器对挂载文件所做的事,第二列是由你的团队运维的部分。
| BotBrowser 负责 | 你的团队负责 |
|---|---|
加载通过 --bot-profile 传入的 Profile 文件 | 共享内存、CPU 和内存限制,以及宿主机 |
启动时从 --bot-profile-dir 中选取 Profile | 虚拟显示、系统库和基础镜像 |
应用通过 --proxy-server 指定的代理设置 | 代理质量、轮换和网络路由 |
| 使用你提供的用户数据目录运行 | 卷、备份和会话状态的清理 |
| 从挂载的 Profile 启动,无需重新构建镜像 | 调试端口的暴露、网络策略、访问控制和编排 |
排查故障时,可以把这张表当作检查清单。提到内存的崩溃属于容器限制。点名某个库的启动错误属于镜像问题。Profile 路径错误属于挂载问题。它们都不是浏览器 Profile 的问题,也都不能靠更换 Profile 来解决。请把特定网站上的结果排除在部署的完成标准之外,因为它们取决于网站自身的规则,以及你被授权发送的流量。
准备好构建时,请下载 BotBrowser,并确认你的套餐包含 Linux 二进制文件。对于更大的集群,企业页面介绍了套餐选项。CLI 用法示例列出了这里用到的选项及其组合,在扩展 entrypoint 时会很有用。
来源
实践结论
把五项容器设置写进镜像和启动命令,而不是写进没人翻看的运维手册:--shm-size 的取值、带 DISPLAY=:10.0 的 Xvfb 步骤、Ubuntu 22.04 基础镜像、--init,以及每个实例一个用户数据卷。然后先启动一个容器,用 --bot-profile 挂载配置文件,确认发布在回环地址上的 9222 端口有响应,再用 Compose 扩展同一份配置。
本文属于指南: 浏览器部署与发布:运维决策地图