How to Build a GitLab CI/CD Testing Pipeline with Selenium
Build a GitLab pipeline that runs Selenium browser tests, publishes reports and failure evidence, and scales to remote browsers with Selenium Grid.
A GitLab CI/CD pipeline for Selenium prepares or deploys the application under test, runs WebDriver checks on a runner, and saves reports and failure evidence as job artifacts. For a small suite, run tests in a job with access to a browser. Use Selenium Grid when remote execution, parallel sessions, or broader browser and operating system coverage justify the extra infrastructure.
This guide uses Python, pytest, and a separately managed Selenium Grid as its concrete example. It assumes your project already has a test target and a runner that can reach both that target and Grid. GitLab runner executors, deployment processes, and browser images vary, so the YAML is an example to adapt rather than a universal recipe.
1. Understand the pipeline flow
GitLab reads pipeline configuration from .gitlab-ci.yml. Stages set the broad order of work; jobs in the same stage can run in parallel. A typical browser-test flow is:
- Prepare: Build or deploy the application and make a stable test URL available.
- Test: Start the Selenium client and run browser checks against that URL.
- Collect: Save JUnit results, screenshots, and useful logs as artifacts, including when a test fails.
- Clean up: Remove temporary environments if your deployment created them.
GitLab pipeline stages and job ordering are described in its CI/CD pipelines documentation. You can add needs to express dependencies and reduce waiting, but keep the dependency graph understandable.
2. Choose where the browser runs
Browser available to the job
For a small, single-browser suite, use a job image that contains your test runtime and a browser, or configure a browser service the job can reach. Selenium WebDriver uses language bindings to control browsers. Selenium Manager can manage drivers through Selenium bindings, but it cannot make a missing browser available; the browser still has to exist in the execution environment. See Selenium installation and the Selenium documentation.
GitLab jobs can specify an image and service containers. Service reachability depends on the job’s network arrangement, alias, port, and runner configuration. GitLab documents the general mechanisms in Docker images and services; verify the selected browser image, readiness behavior, and connectivity with your actual runner.
Remote browsers with Selenium Grid
Grid routes WebDriver commands from the test client to remote browser instances. Standalone mode accepts RemoteWebDriver requests at http://localhost:4444 by default when the client and Grid share a host or network namespace. In CI, use the endpoint reachable from the job, which may be a service alias or an infrastructure-provided URL. Grid is useful for parallel sessions or browser and operating system coverage that warrants its added setup and capacity planning. See Selenium Grid, Getting started with Grid, and When to use Grid.
Keep Grid private. Selenium warns that an exposed Grid can provide access to infrastructure and internal applications, and says it must be protected from external access using appropriate firewall permissions. Use network controls and do not expose an unauthenticated Grid endpoint publicly.
| Choice | Good fit | Plan for |
|---|---|---|
| Browser in or alongside the job | A modest suite targeting one browser | Runner image maintenance, browser availability, and readiness |
| Remote Selenium Grid | Remote execution, parallel sessions, or a browser/OS matrix | Endpoint access, Grid security, capacity, and service health |
3. Add a runnable Python Selenium test
The example below uses pytest and RemoteWebDriver. The Grid endpoint and test URL come from environment variables so they can differ by branch or environment. It expects Grid to be provisioned separately and reachable from the job.
requirements.txt
pytest
selenium
tests/test_homepage.py
import os
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
@pytest.fixture
def driver():
grid_url = os.environ["SELENIUM_REMOTE_URL"]
options = Options()
options.add_argument("--headless")
browser = webdriver.Remote(command_executor=grid_url, options=options)
try:
yield browser
finally:
browser.quit()
def test_homepage_has_title(driver):
driver.get(os.environ["APP_BASE_URL"])
assert driver.title.strip()
This is a smoke test: it checks that the target responds with a non-empty page title. Replace the assertion with checks meaningful to your application. If you use a browser image with different capabilities, configure the matching options and browser name for that environment.
4. Configure the GitLab pipeline
Here is an example .gitlab-ci.yml. It uses a Python job image and an already available Grid URL. Set APP_BASE_URL and SELENIUM_REMOTE_URL as project or group CI/CD variables. The endpoint must be reachable from the runner. The example runs on pushes and merge requests, installs dependencies, waits for the Grid status endpoint, runs pytest, and keeps JUnit output and screenshots as artifacts.
stages:
- test
selenium_tests:
stage: test
image: python:3.12
variables:
APP_BASE_URL: "https://staging.example.com"
SELENIUM_REMOTE_URL: "http://selenium-grid.internal:4444"
script:
- python -m pip install --disable-pip-version-check -r requirements.txt
- python - <<'PY'
import os
import time
import urllib.error
import urllib.request
endpoint = os.environ["SELENIUM_REMOTE_URL"].rstrip("/") + "/status"
deadline = time.monotonic() + 90
while time.monotonic() < deadline:
try:
with urllib.request.urlopen(endpoint, timeout=3) as response:
if response.status == 200:
print("Selenium Grid status endpoint is reachable")
break
except (OSError, urllib.error.URLError):
time.sleep(2)
else:
raise SystemExit(f"Selenium Grid did not become reachable: {endpoint}")
PY
- mkdir -p artifacts
- pytest -q --junitxml=artifacts/junit.xml
artifacts:
when: always
expire_in: 7 days
reports:
junit: artifacts/junit.xml
paths:
- artifacts/
rules:
- if: '$CI_PIPELINE_SOURCE == "push"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
Replace the example staging hostname and internal Grid hostname. The YAML does not deploy the application or create Grid; add a prepare/deploy job or use your existing deployment process if needed. Ensure its output URL is passed to the test job, and that the runner can route to both endpoints. GitLab Docker jobs run scripts in the project build directory, so the relative paths above assume requirements.txt and tests/ are in the repository root.
The status check only confirms the endpoint returns HTTP 200; it does not guarantee a requested browser is available or that the application is ready. A failed session creation should be diagnosed from Grid and job logs as well as the status check.
Use a browser service instead
If you choose a Selenium/browser service container, declare it under services and set SELENIUM_REMOTE_URL to its reachable alias and port. The exact image, tag, alias, readiness behavior, and runner networking must match your environment. GitLab’s service documentation explains the general configuration, but does not prescribe one Selenium-specific image recipe. Verify those details before relying on the job.
Application deployment and job dependencies
For an application deployed by the same pipeline, add a deployment or test-environment job before the test stage, then provide its URL to Selenium. Stages run sequentially by default, while jobs in one stage may run concurrently. Use needs when the test job should start as soon as its deployment dependency completes. Add cleanup for temporary environments where appropriate.
5. Save reports and failure evidence
GitLab can display supported test reports in merge requests when you publish them in the configured report format. The example marks JUnit XML as a report and also keeps the artifact directory for download. See GitLab testing and job artifacts.
To capture a screenshot on test failure, add a pytest hook or fixture that saves the current browser screenshot before quitting. For example, wrap the test body in a failure handler that calls driver.save_screenshot("artifacts/failure.png"), then re-raises the exception so the job still fails. Create the artifact directory before the test starts. Keep artifacts paths and retention periods deliberate: screenshots and logs can contain personal data, credentials, or other sensitive content. Do not print secrets or put them in artifacts.
6. Store secrets and control versions
Use protected and masked CI/CD variables according to your project’s secret-management policy for credentials or private endpoints. Do not echo secret values in scripts. For GitLab 17.7 and later, GitLab recommends pipeline inputs over passing pipeline variables; pipeline variables have high precedence and can override variables defined elsewhere. See the pipeline documentation for version-specific guidance.
Pin the Python, Selenium client, Grid server, and browser image versions appropriate to your compatibility needs. Review release notes when updating. The Selenium downloads page listed 4.49.0 as stable on September 9, 2026; verify the current release when publishing or adopting this guide at Selenium downloads.
If your pipeline builds or launches containers with Docker-in-Docker, check the runner executor prerequisites. GitLab’s documented Docker and Kubernetes executor setup requires privileged mode for that configuration; privileged mode is not the only way to arrange builds, and the right approach depends on infrastructure policy. GitLab recommends pinning the Docker-in-Docker image version and using TLS where possible. See Use Docker-in-Docker.
7. Plan capacity, performance, and reliability
- Parallelism: More sessions can reduce wall-clock time only when the runner and Grid have spare capacity. Start with measured concurrency and increase gradually.
- Resource sizing: Selenium’s Grid guide gives 1 CPU and 1 GB RAM per browser as a reference, not a universal guarantee. Workload, browser, page, and host all matter; measure performance continuously.
- Waits: Prefer explicit waits for application conditions over fixed sleeps. A fixed delay can waste time on fast runs and still be too short on slow ones.
- Readiness: Check Grid availability and application readiness separately. A reachable Grid does not imply a browser session can start or the application is usable.
- Isolation: Avoid sharing mutable test data between parallel tests. Use isolated accounts or fixtures where the application allows it.
- Retries: Retries can help identify intermittent infrastructure failures, but they can also hide flaky tests. Preserve first-failure evidence and track repeat failures.
- Artifacts: Retain enough logs and screenshots to diagnose failures, while controlling storage through appropriate paths and expiration.
8. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Connection refused or timeout to Grid | The endpoint is not reachable from the job, the alias/port is wrong, or Grid is not ready. | Check runner routing, service alias and port, and Grid logs. Wait for readiness with a bounded timeout. |
| Session creation fails | Grid has no matching browser capacity, or requested capabilities do not match available browsers. | Inspect Grid status and session logs; align requested browser options with the configured nodes. |
| Browser or driver not found | The job environment does not contain the browser, or its driver setup is unavailable. | Use a browser-enabled environment or remote Grid. Selenium Manager can manage drivers through bindings but does not install a missing browser into an unsuitable environment. |
| Application URL cannot be opened | The test target is unavailable to the runner or the URL is wrong. | Confirm deployment completed, the URL is passed correctly, and runner network access and DNS are configured. |
| Tests pass locally but fail in CI | Different browser/runtime versions, timing, timezone, locale, or network conditions. | Pin compatible versions, capture failure screenshots and logs, and replace fixed sleeps with condition-based waits. |
| JUnit report is missing | Pytest failed before writing XML, the path differs, or the report configuration does not match. | Check job output and ensure the same report path is used for pytest and artifacts:reports:junit. |
| Artifacts are absent after failure | Artifact path is incorrect or collection is not configured to run after failures. | Use when: always, create the directory before testing, and verify the generated files match the configured paths. |
| Docker-in-Docker service fails to start | Runner executor or privilege/TLS configuration does not meet the chosen setup’s requirements. | Review GitLab’s Docker-in-Docker guidance and your runner policy; use pinned images and the supported configuration for that executor. |
9. Or skip the browser setup
If your CI job needs a screenshot artifact rather than browser interaction, ScreenshotNeo can capture a page with one API request. It is a website screenshot API and MCP server from ScreenshotNeo. See the API documentation for options and response details.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Store the API key as a protected CI/CD variable and use your own target URL. ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.
Sign up free and get 1,000 screenshots a month with no card.
10. Frequently asked questions
Do I need Selenium Grid for GitLab CI?
No. A browser available to the job is enough for a modest single-browser suite. Grid is useful when remote execution, parallel sessions, or wider browser and operating system coverage justify it.
Can jobs run Selenium tests in parallel?
Yes, provided the runner and browser infrastructure have capacity and tests do not collide over shared state. GitLab jobs in a stage can run concurrently; Grid can route sessions to remote browser instances.
What should I keep after a failed run?
At minimum, retain a machine-readable test report and enough logs to identify the failing test. Add screenshots when they help explain browser state, and set artifact access and expiry to suit the data they contain.
Does Selenium Manager remove the need for a browser image?
No. It can manage drivers through Selenium bindings, but the execution environment still needs an available browser.


