一、为啥要把Selenium和Docker放在一起

很多搞测试的朋友都头疼一件事:每次搭一套 Selenium 测试环境,光装浏览器、驱动、环境变量就能折腾半天。更别提换台电脑或者要跑不同浏览器版本,全得重来一遍。时间都花在配置上了,真正测试的时间反而没多少。

这时候 Docker 就站出来说 “让我来”。它能把整个 Selenium 运行环境打包成一个轻量级容器,你只需要一条命令就能拉起来一个带 Chrome 或者 Firefox 的测试环境。而且 Docker 的环境隔离能力让不同版本互不打架,想切版本就跟换个容器一样简单。

1.1 测试环境的老大难问题

先说说我们以前怎么干的。在一台开发机上装 Chrome,再装对应版本的 ChromeDriver,然后设置 PATH,搞不好还要装个 Java 或者 Python 的库。等换到另一台机器,又要重复一遍,万一操作系统不一样,还得调各种依赖。团队里每个人环境不同,测出来的结果也经常不一致,你明明能跑过的脚本,到别人机器上就报错。

更头疼的是,我们有时候需要同时测 Chrome 80 和 Chrome 90,或者同时跑 Firefox 和 Edge。平常手动切换真的很费劲,而且你要是想用 Selenium Grid 玩并发,还得自己搭 Hub 和 Node,那配置量又大了好几倍。

1.2 Docker 能帮上什么忙

Docker 最大的好处就是 “一次构建,到处运行”。你只需要拉一个官方提供的 Selenium 镜像,里面已经装好了浏览器、驱动和 Selenium Server,不管你的电脑是 Windows、Mac 还是 Linux,跑起来的测试环境都一模一样。

而且 Docker 容器启动很快,几秒钟就能就绪。测试完直接删除容器,不留下任何乱七八糟的残留。要是想跑多个浏览器版本,开几个不同标签的容器就行,不会互相干扰。配合 Docker Compose 或者 Selenium Grid 官方镜像,搭建并发测试环境变得轻而易举。

二、准备工作:选对 Docker 镜像

2.1 官方镜像推荐

Selenium 官方在 Docker Hub 上提供了好几个镜像,我用得最多的是这几个:

  • selenium/standalone-chrome – 单个 Chrome 节点的独立镜像,开箱即用。
  • selenium/standalone-firefox – 类似,不过用的是 Firefox。
  • selenium/standalone-edge – 针对 Edge 浏览器。
  • selenium/hub – 配合后面的 Node 镜像,组成 Grid 集群。

这些镜像都是官方维护的,更新很及时,而且会跟着浏览器最新版本走。你不用担心驱动版本不匹配的问题,因为它们都已经帮你调好了。

2.2 镜像版本怎么挑

镜像的标签通常像 selenium/standalone-chrome:4.15.0-20231227,前面是 Selenium 版本号,后面是镜像构建日期。我一般直接用 latest 标签来获取最新版,但如果项目有特殊要求,比如必须用 Chrome 某个旧版,就需要去查对应的镜像标签。

另外还有几个带 debug 的镜像,比如 selenium/standalone-chrome-debug,里面加了 VNC 服务,你可以远程连进去看浏览器到底在干啥,调试非常方便。刚开始学的时候我特别爱用这个,因为能看到脚本操作浏览器的过程。

三、启动容器,跑起 Selenium

3.1 最简单的启动命令

要启动一个 Selenium 独立 Chrome 容器,只需要一行命令:

# 拉取最新版 standalone-chrome 镜像
docker pull selenium/standalone-chrome:latest

# 启动容器,映射 4444 端口到宿主机
docker run -d -p 4444:4444 --name selenium-chrome selenium/standalone-chrome:latest

解释一下:

  • -d 表示后台运行。
  • -p 4444:4444 把容器的 4444 端口映射到宿主机的 4444 端口,这样就能通过 http://localhost:4444 访问 Selenium Server。
  • --name 给容器起个名字,方便后面管理。

启动后,你可以用 Python 写个脚本连上去试试:

# 技术栈:Python + Selenium
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

# 配置远程连接的选项
chrome_options = Options()
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--headless")  # 无头模式,不显示浏览器窗口

# 连接到 Docker 中的 Selenium Server
driver = webdriver.Remote(
    command_executor="http://localhost:4444/wd/hub",  # Docker 映射的地址
    options=chrome_options
)

try:
    # 打开百度
    driver.get("https://www.baidu.com")
    print(f"页面标题是:{driver.title}")  # 打印标题确认环境正常
finally:
    driver.quit()  # 关闭会话

如果你用的是 Firefox,只要把镜像换成 selenium/standalone-firefox,然后 chrome_options 换成 firefox_options 就行。

3.2 挂载卷让测试脚本飞进去

上面那种方式,测试脚本必须从宿主机连接容器,脚本文件放在宿主机。但有时候我们想把脚本直接放到容器里运行,或者需要容器访问宿主机上的某个文件夹(比如下载目录)。这时候就要用挂载卷。

# 启动时挂载本地文件夹到容器内的 /home/seluser/scripts
docker run -d -p 4444:4444 \
  -v /path/on/host/scripts:/home/seluser/scripts \
  --name selenium-mount selenium/standalone-chrome:latest

挂载之后,如果你在容器里运行 shell,就能直接看到宿主机的文件。但 Selenium 容器通常不是拿来跑脚本的,而是作为远程服务。所以挂载更常见的用途是让容器里保存截图、日志或者下载文件到宿主机。

比如你想让测试脚本生成的截图保存到宿主机:

# 技术栈:Python + Selenium
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time

chrome_options = Options()
chrome_options.add_argument("--no-sandbox")

driver = webdriver.Remote(
    command_executor="http://localhost:4444/wd/hub",
    options=chrome_options
)

driver.get("https://www.example.com")
time.sleep(2)

# 截图保存在容器内的 /home/seluser/screenshots,因为挂载了卷,所以实际会出现在宿主机目录
driver.save_screenshot("/home/seluser/screenshots/test.png")

driver.quit()

然后在宿主机上就能在 /path/on/host/scripts 里找到截图(因为挂载的路径映射了)。注意容器里的用户是 seluser,默认对挂载目录有写权限,但有时会因为权限问题报错,可以加 :Z 或者用 --user root 来解决。

3.3 端口映射和网络设置

如果你一台机器上要跑多个 Selenium 容器(比如一个 Chrome,一个 Firefox),就不能都用 4444 端口,需要映射不同的端口:

docker run -d -p 4444:4444 --name chrome-v1 selenium/standalone-chrome:latest
docker run -d -p 4445:4444 --name firefox-v1 selenium/standalone-firefox:latest

这样 Chrome 的 Selenium 服务在 localhost:4444,Firefox 在 localhost:4445。脚本里只要把 command_executor 的端口改一下就能连到对应的浏览器。

另外,如果你想在多个容器之间进行网络通信(比如后面说到 Grid 场景),可以创建一个自定义网络:

docker network create selenium-net

docker run -d --network selenium-net --name hub selenium/hub:latest
docker run -d --network selenium-net -e SE_EVENT_BUS_HOST=hub -e SE_EVENT_BUS_PUBLISH_PORT=4442 -e SE_EVENT_BUS_SUBSCRIBE_PORT=4443 --name node-chrome selenium/node-chrome:latest

这里不展开 Grid 细节,后面会讲。

四、并发管理:跑多份测试

4.1 用 Docker Compose 组织多个容器

当你需要同时启动多个容器(比如一个 Hub+ 多个 Node),用手打命令太容易搞错。Docker Compose 可以帮你用 YAML 文件描述整个环境,一条命令全部搞定。

下面是一个简单的示例,同时启动一个 Chrome 节点和一个 Firefox 节点,都注册到同一个 Hub 上:

# 技术栈:docker-compose.yml
version: "3"

services:
  selenium-hub:
    image: selenium/hub:latest
    container_name: selenium-hub
    ports:
      - "4442:4442"
      - "4443:4443"
      - "4444:4444"
    environment:
      - SE_SESSION_REQUEST_TIMEOUT=300
      - SE_NODE_MAX_SESSIONS=5

  chrome-node:
    image: selenium/node-chrome:latest
    container_name: chrome-node
    depends_on:
      - selenium-hub
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
      - SE_NODE_MAX_SESSIONS=2
    volumes:
      - /dev/shm:/dev/shm

  firefox-node:
    image: selenium/node-firefox:latest
    container_name: firefox-node
    depends_on:
      - selenium-hub
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
      - SE_NODE_MAX_SESSIONS=2
    volumes:
      - /dev/shm:/dev/shm

然后执行:

# 在 docker-compose.yml 所在目录启动
docker-compose up -d

这样三个容器就都起来了。Hub 的 4444 端口映射到宿主机,所有测试脚本连到 http://localhost:4444/wd/hub,Hub 会自动把任务分配给 Chrome 或者 Firefox 节点。

4.2 让 Selenium Grid 派上用场

刚才的 Compose 其实已经搭建了一个 Selenium Grid。Grid 的最大优势是支持并发测试。比如你有 5 个测试用例,可以同时发给 Hub,Hub 根据节点配置(SE_NODE_MAX_SESSIONS)决定能同时跑多少个。在脚本里,你只需要创建一个线程池,每个线程创建一个 Remote WebDriver 连到同一个 Hub 地址就行。

# 技术栈:Python + Selenium + 多线程
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import threading
import time

def run_test(browser_name):
    """
    模拟一个测试任务,browser_name 可以是 'chrome' 或 'firefox'
    """
    opts = Options()
    opts.add_argument("--headless")
    opts.add_argument("--no-sandbox")
    opts.add_argument("--disable-dev-shm-usage")
    
    if browser_name == 'firefox':
        from selenium.webdriver.firefox.options import Options as FirefoxOptions
        opts = FirefoxOptions()
        opts.add_argument("--headless")

    # 连接到 Hub(端口 4444)
    driver = webdriver.Remote(
        command_executor="http://localhost:4444/wd/hub",
        options=opts
    )
    try:
        driver.get("https://www.baidu.com")
        print(f"[{browser_name}] 页面标题: {driver.title}")
        time.sleep(2)  # 模拟一点操作
    finally:
        driver.quit()

# 启动 5 个线程并发测试
threads = []
for i in range(5):
    browser = 'chrome' if i % 2 == 0 else 'firefox'
    t = threading.Thread(target=run_test, args=(browser,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print("全部测试完成")

运行这个脚本,你就会看到 Hub 把任务分发给节点。如果节点限制最多 2 个并发会话,那么会排队执行。

4.3 动态伸缩容器数量

测试高峰期你可能需要更多节点,低峰期想省资源。Docker 可以手动启动或停止容器,但更优雅的方式是用 Docker Compose 的 --scale 参数:

# 把 chrome-node 扩展到 3 个
docker-compose up -d --scale chrome-node=3 --scale firefox-node=1

Compose 会自动创建多个同名容器(后面会加序号),它们都会注册到 Hub。这样并发能力就翻倍了。测试完了再缩回去:

docker-compose up -d --scale chrome-node=1 --scale firefox-node=1

不过要注意,--scale 需要服务名在 Compose 里定义,而且不能有端口映射冲突(这里节点没有暴露端口到宿主机,所以安全)。

五、实际应用场景和注意事项

5.1 哪些项目最适合

这种方式特别适合以下场景:

  • 持续集成(CI):比如 Jenkins、GitLab CI 里,每次构建都启动新的容器跑测试,跑完就销毁,保证环境清洁。
  • 多浏览器兼容测试:你需要测 Chrome、Firefox、Edge,甚至不同版本,开几个容器就能覆盖。
  • 快速原型验证:临时想测一个新特性,或者帮同事复现 bug,拉一个容器几秒搞定。
  • 团队协作:大家用同一个镜像,确保环境一致,减少 "我这边能跑" 的问题。

5.2 资源占用和性能优化

Docker 虽然轻量,但一个 Selenium 容器也要占几百 MB 内存(浏览器的开销)。并发跑多了,机器会扛不住。有几个优化建议:

  • 使用 --shm-size=256m 或者挂载 /dev/shm(像上面 Compose 里那样),避免 Chrome 因共享内存不足崩溃。
  • 尽量用无头模式(--headless),减少图形渲染消耗。
  • 限制每个节点的最大会话数(SE_NODE_MAX_SESSIONS),一般设为 2-5,太多会让浏览器抢资源反而变慢。
  • 如果只是做简单的页面截图或 API 测试,可以考虑用更轻的替代方案,比如 Playwright 的无头浏览器。

5.3 常见坑和解决办法

  • 容器启动后无法连接:检查端口映射是否正确,防火墙有没有放行,另外 Hub 或 Node 可能需要几秒钟启动,脚本要等一等。
  • 权限问题:挂载卷时容器内用户 seluser 可能没有写权限,可以在 Dockerfile 里修改,或者挂载时加 :Z 参数(Linux SELinux)。
  • 版本不匹配:Selenium 客户端库版本要和镜像里的 Server 版本一致,否则会出现无法解析新功能的错误。
  • 网络不通:容器网络模式下,如果要跨容器通信(比如脚本也在容器里),建议使用同一个网络。

六、总结

把 Selenium 测试环境用 Docker 容器化,相当于给测试流程装上了快速部署的翅膀。你不再需要为环境配置头疼,不再担心多人环境不一致,更不用手动管理浏览器版本。一条命令就能拉起一个浏览器节点,配合 Compose 或 Grid 可以轻松扩展并发能力。

虽然 Docker 容器也有自己的资源开销,但相比省下来的维护时间和提升的稳定性,这点投入绝对值得。只要注意上面提到的几个要点,比如挂载权限、共享内存、版本匹配,你就能享受 “一键部署,随时销毁” 的清爽测试体验。

以后无论是本地开发调试,还是 CI 流水线自动跑用例,都可以把这套方案用起来,让测试工作更舒心。