How to Use BackstopJS with Docker and Headless Chrome
Run BackstopJS in Docker with headless Chrome. Configure Puppeteer, mount project files, handle permissions and CI output, and troubleshoot browser startup.
To run BackstopJS with Docker and headless Chrome, configure BackstopJS to use the Puppeteer engine, then run backstop test --docker. BackstopJS selects an image matching the version in your project package metadata and mounts the current working directory at /src. Make sure the mounted project directory is writable by the container user so BackstopJS can save screenshots and reports.
1. Configure BackstopJS to use headless Chrome
BackstopJS uses Puppeteer by default. Set the engine explicitly in backstop.json so the browser choice is clear, and put browser-specific settings in engineOptions.
{
"id": "visual-regression",
"viewports": [
{ "label": "desktop", "width": 1365, "height": 768 }
],
"scenarios": [
{
"label": "homepage",
"url": "http://web:3000/",
"selectors": ["document"],
"delay": 500
}
],
"engine": "puppeteer",
"engineOptions": {
"headless": true,
"args": []
},
"paths": {
"bitmaps_reference": "backstop_data/bitmaps_reference",
"bitmaps_test": "backstop_data/bitmaps_test",
"html_report": "backstop_data/html_report",
"ci_report": "backstop_data/ci_report"
}
}
Replace http://web:3000/ with a URL reachable from the container. In a Docker Compose network, a service name such as web can be reachable by other containers; localhost inside the BackstopJS container refers to that container itself, not your host machine.
BackstopJS merges engineOptions with its defaults. The documented defaults include ignoreHTTPSErrors: true and headless behavior based on debugWindow. Review these for your project instead of assuming they are right for every environment. The README also shows setting browser args, headless, and Puppeteer gotoParameters, which control navigation behavior. See the BackstopJS documentation for the current configuration options.
2. Run BackstopJS in its Docker-managed mode
- Install Docker and make sure its daemon is running.
- Run BackstopJS from the project directory that contains its package metadata and configuration.
- Initialize or update references, then run the test command:
npx backstop init
npx backstop reference --docker
npx backstop test --docker
The key command for an existing setup is backstop test --docker. The image version is selected to match the BackstopJS version recorded in the project package metadata. Keep that dependency version pinned and committed so the selected container version is predictable across runs.
The Docker template mounts the current working directory at /src. BackstopJS output paths are relative to its working directory and default to backstop_data, so generated references, test bitmaps, and reports should appear in the project’s mounted directory.
3. Run the image directly when you need more control
BackstopJS documents this general command template for direct invocation:
docker run --rm -it \
--mount type=bind,source="$(pwd)",target=/src \
backstopjs/backstopjs:VERSION \
test
Replace VERSION with the tag matching your project’s BackstopJS version and replace test with the desired BackstopJS command. The Docker Hub overview for the image demonstrates mounting the project at /src and running init, reference, and test. That overview describes a BackstopJS 3.x image, so confirm the behavior and available commands for the exact tag you use; it says openReport is not supported by the image it documents.
For Linux, run as your host UID and GID to avoid files owned by root in the working tree:
docker run --rm -it \
--user "$(id -u):$(id -g)" \
--mount type=bind,source="$(pwd)",target=/src \
backstopjs/backstopjs:VERSION \
test
The image must have a usable home and writable paths for configuration, cache, and browser profiles when the selected user changes. If the container cannot write to the mounted directory or Chrome’s profile location, fix ownership or configure a writable location.
4. Adapt Docker commands for CI
Docker’s -t option allocates a pseudo-terminal. That is useful interactively, but can interfere with piped output in CI systems and IDEs. Remove -t when output is piped:
docker run --rm -i \
--user "$(id -u):$(id -g)" \
--mount type=bind,source="$PWD",target=/src \
backstopjs/backstopjs:VERSION \
test
If your CI runner does not define a meaningful UID/GID or uses a different shell, adapt the user and source-directory expressions to that environment. Ensure the output directory is retained as a CI artifact if you need reports after the job ends.
5. Build a custom container only when needed
A custom image is useful when your test suite needs system packages, a specific browser installation, or project-specific runtime setup. It also makes browser compatibility your responsibility. Puppeteer needs a compatible browser plus the required operating-system shared libraries, and its configuration, cache, and profile paths must be writable.
For a version-specific example, the Docker Hub layer metadata for backstopjs/backstopjs:6.3.25 lists /usr/bin/chromium as the Puppeteer executable path, Node 20.17.0, and linux/amd64 architecture. Treat that path as evidence for that tag only; inspect the exact tag before hard-coding an executable path. The same metadata lists a compressed image size of 1.57 GB, which can affect download time and storage in environments that frequently pull the image.
When using a custom Puppeteer container, follow the Puppeteer troubleshooting guidance for browser dependencies and container permissions. Match the browser to the Puppeteer version used by your BackstopJS installation, and verify all required libraries are present.
6. Troubleshoot common Docker and Chrome failures
| Symptom | Likely cause | Fix |
|---|---|---|
backstop_data or reports are owned by root |
The container ran as root and wrote into the bind-mounted project directory. | On Linux, pass --user "$(id -u):$(id -g)" and ensure the mount remains writable. |
Chrome reports it cannot run as root without --no-sandbox |
Older generated configurations may have Chrome launch settings that assume a root container. The BackstopJS README calls out configurations created before version 3.5. | Regenerate or review the old configuration and run as a non-root user where possible. Disabling Chrome’s sandbox reduces isolation; do not add --no-sandbox as a blanket fix without assessing that tradeoff. |
| Chrome executable not found | The configured path does not exist in this image tag, or the browser was not installed. | Inspect the exact image and Puppeteer configuration. Do not assume a path from another BackstopJS image version. |
| Browser exits immediately or reports missing shared libraries | A custom image lacks operating-system browser dependencies or has an incompatible browser/Puppeteer pairing. | Install the dependencies required by Puppeteer and use a compatible browser version. |
| Chrome cannot create a profile or cache | The selected user cannot write to the browser’s configuration, cache, or profile paths. | Give those paths writable locations or correct their ownership and permissions. |
| Tests cannot reach the target page | The URL uses localhost from inside the container, or the application is not reachable on the container network. |
Use a hostname accessible from the container, such as a Compose service name, and verify the app is running before capturing. |
| CI output behaves strangely or errors on a terminal | Docker allocated a TTY while stdout or stderr is piped. | Remove -t; retain -i only if the command needs stdin. |
openReport is unavailable |
The Docker Hub overview’s documented image does not support that command; the limitation may differ for another tag. | Check the documentation and contents for your exact image tag, or open the generated HTML report through your CI artifact workflow. |
| Reference and test screenshots differ across machines | Different fonts, browser builds, operating-system libraries, page state, or timing can affect rendering. Docker helps control the environment but does not guarantee identical output. | Pin the BackstopJS image version, control the page’s data and readiness, and use the same image and settings for reference and test captures. |
7. Improve repeatability, performance, and cost control
- Pin versions: keep BackstopJS in project metadata and use its matching Docker image. Avoid relying on an unpinned tag when stable comparisons matter.
- Keep the capture environment consistent: use the same image, fonts, browser, viewport definitions, and application state for reference and test runs. Docker reduces environment differences; it does not erase them.
- Make page readiness explicit: use scenario waits and navigation settings that match the application. A fixed delay can help with known asynchronous rendering, but unnecessary delays lengthen every capture; prefer a reliable readiness condition when your setup supports one.
- Persist what you need: the bind mount preserves BackstopJS output in the project directory. In CI, retain reports and relevant bitmaps as artifacts rather than expecting a removed
--rmcontainer to keep them. - Account for image pulls: the 6.3.25 layer metadata reports a 1.57 GB compressed image. Reusing cached layers on persistent runners can avoid repeated downloads; ephemeral runners may incur repeated transfer and storage time.
- Budget compute rather than per-shot API charges: this workflow runs a browser in your container. Its practical cost comes from runner time, image storage and transfer, and the number of parallel jobs; the dossier provides no benchmark for capture speed.
BackstopJS documentation notes that the same page can render differently across environments, including text. Use Docker to standardize the capture environment, and investigate font availability, browser/image versions, and page state when diffs persist.
Or skip the browser setup
For a one-request screenshot through an API, ScreenshotNeo accepts a URL and returns an image or PDF. It is a separate capture option from running BackstopJS visual regression tests: use BackstopJS when you need its reference-versus-test workflow, and consider the API when your task is simply to capture a page. See the ScreenshotNeo API documentation for configuration details.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie banners are accepted and removed, along with known newsletter popups and chat widgets, before the shot; each cleanup step can be turned off.
- Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed. Response headers report the page verdict and billing status.
- An MCP server lets AI agents use
take_screenshot,get_page_info, andcapture_pdf. - The free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan.
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
Frequently asked questions
How do I use headless Chrome with BackstopJS?
Use the Puppeteer engine in backstop.json and run BackstopJS in its Docker mode with backstop test --docker. Set engine options only as needed for your browser and navigation behavior.
Where do BackstopJS screenshots go in Docker?
They are written under the configured paths, which default to backstop_data relative to the working directory. BackstopJS’s Docker command bind-mounts that directory into the container at /src.
Does Docker make screenshots identical across machines?
No. It helps standardize the environment, but browser versions, fonts, operating-system dependencies, and page state can still change rendering.
Can I use openReport in the BackstopJS image?
The Docker Hub overview describes an image where openReport is unsupported. Check the exact tag you use because the overview is scoped to its documented image and describes BackstopJS 3.x.


