ScreenshotNeo

BlogHow-to

How to Run Fast Cypress Tests in a Tiny Docker Image

Choose a lean Cypress image that still supports your browser, then cut CI time with reproducible installs, caching, and balanced parallel runs.

By the ScreenshotNeo team4 October 202610 min read

Short answer: Start with the Cypress image family that satisfies your Node, Cypress, browser, and CPU architecture requirements. Then speed up CI with lockfile-based installs, a persistent Cypress binary cache, and shorter or better-balanced tests. A smaller Docker image can reduce pulls and setup time, but it does not make browser tests execute faster by itself.

This guide shows how to choose an image, build a reproducible CI job, cache safely, diagnose common failures, and decide whether parallel runners are worth their cost. Exact tags and browser combinations change, so verify them against the current Cypress Docker image documentation before pinning a workflow.

1. Choose the smallest image that meets the test requirements

First list what the job actually needs: Node version, Cypress version, browser, operating system dependencies, and CPU architecture. The image family controls what is preinstalled:

Image family What it provides When to consider it
cypress/base Debian, Cypress prerequisites, Node.js, npm, and Yarn v1 When your required browser setup is limited and the documented combination works for your project.
cypress/browsers The base image plus installed browsers When the tests need Chrome, Firefox, or Edge. Availability differs by architecture and tag.
cypress/included The browsers image plus a globally installed, fixed Cypress version When its bundled Cypress, Node, and browser combination fits.
cypress/factory A base operating-system image for generating customized images When published combinations do not meet the required versions, and you can maintain the custom image.

Do not pick cypress/base just because it sounds smallest. If the selected browser is missing, adding it and its dependencies later can cost more time and maintenance than using a browser image. Cypress documents generally available Linux/amd64 and Linux/arm64 support, but browser coverage varies: for example, its documentation describes different Chrome, Firefox, and Edge availability on arm64. Check the exact tag and platform before choosing.

The Cypress Docker images include the required dependencies for their documented combinations. A custom base image does not inherit that guarantee; use Cypress’s supported operating-system guidance and verify all browser and Cypress prerequisites when building one.

2. Establish a reproducible baseline

Before optimizing, capture a baseline from the same CI runner type and workflow. Record the image pull time, dependency installation time, Cypress binary cache hit or miss, application startup time, test duration, final image size, and runner cost. Repeat enough runs to distinguish a real change from normal CI variation.

Pin a real supported tag after checking the current registry. The following Dockerfile uses a placeholder intentionally; replace it with a verified tag matching the required Node and browser versions.

# Dockerfile
# Replace this placeholder with a verified cypress/browsers tag.
FROM cypress/browsers:<verified-node-and-browser-tag>
WORKDIR /app

# Copy manifests first so dependency layers can be reused when source changes.
COPY package.json package-lock.json ./
RUN npm ci

COPY . .
CMD ["npx", "cypress", "run", "--browser", "chrome"]

This example assumes Chrome is present in the chosen image and that the project has a committed npm lockfile. If you use another package manager, use its frozen-lockfile install mode and cache its package-manager cache. Keep the Cypress version in the project dependencies explicit so the project and CI agree.

For many CI systems, it is simpler to use the Cypress image as the job container and let the CI cache persist between runs than to build a fresh image containing application dependencies on every commit. Docker layer caching and CI dependency caching solve different parts of setup; measure which layer is actually slow.

3. Cache the Cypress binary and package-manager downloads

A Cypress installation has an npm package and a separate platform-specific binary. On Linux, Cypress stores the binary in ~/.cache/Cypress. Cypress’s performance guide describes the binary as over 100 MB and recommends caching this directory as well as the package-manager cache. A cache hit avoids downloading the binary again.

Use npm ci with a committed lockfile for npm projects. Cache the package manager’s download cache, not node_modules as a general shortcut: Cypress warns that caching node_modules can bypass integrity checks and the binary’s postinstall download. The official Cypress GitHub Action handles npm and Cypress binary caching automatically, according to Cypress’s guide; verify the current action version and configuration for your workflow.

A generic GitHub Actions cache outline looks like this. The cache action version is an example and should be checked against your repository’s current action policy.

- uses: actions/checkout@v4
- uses: actions/setup-node@v4
  with:
    node-version: '22'
    cache: npm
- name: Cache Cypress binary
  uses: actions/cache@v4
  with:
    path: ~/.cache/Cypress
    key: cypress-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
    restore-keys: |
      cypress-${{ runner.os }}-
- run: npm ci
- run: npx cypress verify
- run: npm run build
- run: npx cypress run --browser chrome

Choose a cache key that changes when the dependency lockfile or relevant platform changes. Avoid excessively broad keys that restore stale binary versions. Some CI systems provide their own cache syntax; the cache path and keying principle remain the same. cypress verify can help make binary availability failures visible before the full suite starts.

4. Run a complete CI job

Here is a GitHub Actions example using a browser image as the job container. The image tag is deliberately a placeholder because published browser and Node combinations are version-sensitive. If Firefox is used, Cypress’s GitHub Actions documentation says to run with a non-root user such as 1001; check the current guidance for the selected browser and image.

name: Cypress
on: [push, pull_request]
jobs:
  e2e:
    runs-on: ubuntu-24.04
    container:
      image: cypress/browsers:<verified-node-and-browser-tag>
      options: --user 1001
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: npm
      - uses: actions/cache@v4
        with:
          path: ~/.cache/Cypress
          key: cypress-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
          restore-keys: cypress-${{ runner.os }}-
      - run: npm ci
      - run: npx cypress verify
      - run: npm run build
      - run: npx cypress run --browser chrome

Use a non-root user only when the image, browser, and CI configuration support it; do not copy a user option blindly between environments. GitHub Actions supports container jobs on Linux runners. For other CI providers, use their equivalent job-image and cache configuration.

5. Reduce the test time itself

Separate setup time from test execution. A binary cache improves setup after the first run, but cannot speed up a slow assertion, network wait, or application response. Cypress’s published guidance says individual tests under three seconds are excellent; three to ten seconds can be acceptable for many end-to-end tests; ten to thirty seconds merit investigation; and over thirty seconds is poor. Component tests should consistently run under two seconds. Treat these as Cypress guidance ranges, not a guarantee for every suite.

  1. Find the slowest tests and specs in CI reports.
  2. Look for fixed sleeps that can become a condition-based wait, repeated expensive setup, unnecessary page reloads, and avoidable external network dependencies.
  3. Check whether the application server, database, or test data setup is the bottleneck before changing the Docker image.
  4. Compare test duration across runs and inspect whether CPU or memory saturation is slowing the browser.
  5. Split long specs when it creates sensible independent work units; avoid producing a large number of tiny specs with disproportionate startup overhead.

6. Parallelize only when the suite can use it

Cypress Cloud can distribute whole spec files across multiple CI machines for recorded runs. The workflow requires recording results and using parallelization. Cypress uses duration estimates to balance work, so similarly sized specs help; one unusually long spec can leave other machines idle. This reduces total elapsed suite time, but it does not make a single test run faster.

For example, Cypress’s performance guide reports a Kitchen Sink run decreasing from 1:51 serially to 59 seconds with two machines, a 53% reduction. This is Cypress’s illustrative example, not an expected result for your project. Browser launch and video encoding overhead can limit additional gains, and more machines add runner cost.

# Requires Cypress Cloud recording and project configuration
npx cypress run --record --parallel

On GitHub Actions, a matrix can provision worker jobs, but every worker must use a compatible environment and the same container where required by the workflow. Start with a small number of runners, inspect utilization and spec balance, then compare saved wall-clock time with the extra runner cost. If your suite has only a few specs or one dominant long spec, improve the test breakdown before scaling out.

7. Or skip the browser setup

If your task is capturing a webpage rather than testing application behavior, a browser automation container may be unnecessary. ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a screenshot or PDF; see the API documentation.

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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot, and each cleanup step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing; response headers report the page verdict and billing status. An MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000.

Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.

8. Troubleshooting

Symptom Likely cause Fix
Browser executable not found The chosen image does not contain that browser, or its tag/platform combination differs. Use a verified cypress/browsers or cypress/included tag, or install the documented browser dependencies in a supported custom image.
Cypress binary missing or verification fails The binary cache was not restored, the install step was skipped, or a stale cache was restored. Run npm ci, check ~/.cache/Cypress and the cache key, then run npx cypress install or npx cypress verify to expose the underlying issue.
Tests work locally but fail in CI Different Node, Cypress, browser, architecture, environment variables, or server readiness behavior. Pin and align versions, inspect the job platform, and make the server readiness check explicit before Cypress starts.
Firefox fails under the container user Some documented CI combinations require a non-root user. Follow Cypress’s current Firefox and image instructions; the official GitHub Actions example uses user 1001.
Image is small but job is still slow Test runtime, package installation, app build, or server startup dominates rather than image transfer. Time each phase separately, cache the Cypress and package-manager downloads, and optimize the slow phase.
Parallel workers finish at very different times Spec durations are unbalanced or one spec dominates. Inspect per-spec durations and split long specs where appropriate; add workers only after balancing the work.
More parallel workers do not reduce duration Browser startup, encoding, CPU, memory, or coordination overhead dominates. Check runner utilization and compare marginal time saved with runner cost; reduce workers if they are idle or competing for resources.
Custom image fails with missing libraries An arbitrary Linux base lacks Cypress or browser prerequisites. Use an official Cypress image or follow the supported OS prerequisites and validate the complete browser/Cypress combination.

9. Performance, reliability, and cost checklist

  • Image: compare pull time and image size for compatible tags on the actual CI runner; do not infer test speed from image size.
  • Versions: pin Node, Cypress, and browser combinations after checking current tags and architecture support.
  • Installs: commit the lockfile and use npm ci or the package manager’s frozen mode.
  • Caches: persist the Cypress binary and package-manager cache; key caches to lockfile and platform changes; avoid treating node_modules as a safe general cache.
  • Reliability: verify browser availability, binary installation, and server readiness explicitly. Keep logs for cache misses and failed install steps.
  • Runtime: fix slow tests and setup before adding runners. Confirm the suite has enough independently schedulable specs.
  • Cost: include image storage and pulls, build time, cache storage, and extra CI machine minutes in the comparison. Parallelism buys elapsed time with additional compute; gains are not linear.

FAQ

Does a tiny image make Cypress tests faster?

It can reduce image transfer and setup time. It does not directly reduce the time a browser spends executing tests.

Should I use cypress/included?

Use it when its bundled Cypress, Node, and browser versions fit your requirements. Otherwise choose a matching browsers image or a maintained custom combination.

Can I cache node_modules instead?

Cypress advises against it as a general strategy. Cache the package manager’s cache and Cypress binary, then perform a reproducible install.

Does Cypress parallel mode split one spec across machines?

The documented Cloud workflow distributes spec files among machines. A single spec remains one work unit, so balance or split long specs when it is practical.

What is the smallest valid image?

There is no universal answer in the cited Cypress guidance. The smallest suitable image depends on supported OS prerequisites and the exact browser, Cypress, Node, and architecture combination.