If a browser test passes on your laptop and fails in Docker, assume the container is part of the test environment, not just the runner. The most useful question is not “what changed in the test?”, but “what changed about rendering, locale, time, or native dependencies between the two environments?”

The usual culprits are surprisingly small: missing fonts, a different timezone, an incomplete locale setup, or browser packages that your image hardening removed. These failures are easy to misread as app bugs because the app is genuinely failing under the container’s conditions. The task is to separate a real product defect from an environment mismatch.

A Docker-only failure is often a reproducibility problem disguised as a test problem. If the same code behaves differently across environments, start by diffing the container, not the assertion.

First, classify the failure

Before changing code, decide which of these buckets the failure belongs to:

Symptom Likely cause What to check first
Text wraps differently, screenshots shift, visual diffs fail Missing fonts or font fallback differences Installed font packages, fontconfig cache
Date, midnight cutoff, cron-based UI, or relative-time assertions fail Timezone drift TZ, /etc/localtime, tzdata
Browser will not launch, crashes on startup, or missing shared library errors appear Missing system packages Browser runtime dependencies, display libraries, sandbox settings
Test passes locally but fails in CI only Image drift or CI-specific base image Exact container digest, browser version, OS packages
Test fails only on hardened images Over-aggressive minimal image Removed fonts, locale data, certificates, NSS, or DBus-related packages

That table is enough to narrow most investigations. The rest of this guide turns each bucket into a repeatable debug path.

Step 1: Prove the failure is container-specific

Do not debug from a vague “works on my machine” comparison. Run the same test in three places if possible:

  1. Your local machine
  2. A local Docker container built from the CI image
  3. The CI job that fails

The goal is to see whether the failure follows the container image, the CI runtime, or both.

A simple way to make this visible is to print the environment alongside the test:

uname -a
cat /etc/os-release || true
locale || true
echo "TZ=$TZ"
date -u
ls -l /etc/localtime || true
fc-match sans || true

If the output differs between local and CI, you have a lead before you touch test code.

For browser tests, also record the browser version and the OS packages installed in the image. Docker images drift when latest tags move or when a base layer changes under you. The Docker documentation recommends pinning base images and understanding image layering before you optimize size or harden the image, which is especially relevant here because “smaller” often means “less complete” (Docker docs).

Step 2: Check fonts before blaming screenshots

Font problems are one of the most common reasons browser tests fail only in Docker, especially when the failure is visual, layout-related, or sensitive to text measurement.

What font issues look like

  • Text renders with a different line break
  • A button shifts by a few pixels because the fallback font has different metrics
  • A screenshot diff flags a whole page because the browser substituted a generic font
  • A character is missing, replaced by a box, or rendered with a fallback glyph

The browser is not inventing these differences. If the container lacks the font the app expects, fontconfig chooses a fallback and the layout changes.

What to inspect

Inside the container, verify that the expected fonts are present and discoverable:

bash fc-match Arial fc-match “Noto Sans” fc-cache -fv

If fc-match resolves to an unexpected family, your image is missing the preferred font or fontconfig has not indexed it. If the image is intentionally minimal, add only the fonts your app actually needs, then rerun the test.

Practical mitigation

  • Install a known font set in the image, rather than relying on browser defaults
  • Rebuild the font cache in the image layer that adds fonts
  • Keep font packages pinned to reduce layout drift
  • Make visual tests tolerate tiny anti-aliasing differences, but do not paper over large metric shifts

For teams doing visual testing, a font mismatch is not a cosmetic problem. It can invalidate the entire comparison. If your visual baseline depends on a font that is not in the container, the baseline is tied to an environment that the CI job does not have.

Step 3: Make timezone explicit, do not inherit it

Timezone bugs are deceptively easy to miss because the app often looks fine until a test crosses midnight, compares local time, or formats a date relative to “now.”

Why Docker changes time behavior

Containers often inherit UTC from the host or use whatever the image ships with. That means a test that passes on a developer laptop in America/New_York may fail in CI if the container defaults to UTC. It also means the browser and the app can disagree if the app reads one timezone and the test runner reads another.

Stabilize the test environment

Set the timezone explicitly in the container and the browser session. A common pattern is to use UTC in CI and set the app or browser under test to a fixed timezone when the scenario depends on local time.

bash TZ=UTC date

For application code, prefer a fixed timezone in tests rather than “whatever the container defaults to.” If the browser automation framework supports timezone emulation, use it for date-sensitive flows so the test expresses the intended user locale instead of the CI host.

Failure modes to watch for

  • Relative timestamps, such as “2 hours ago,” change during the run
  • A date picker opens to the wrong day because midnight has passed in one timezone but not the other
  • A scheduled job or expiry warning appears on the previous or next day in CI

If a test assertion depends on local time, document the timezone as part of the test setup. Otherwise, the environment is silently part of the assertion.

Step 4: Check locale and language, not just timezone

Locale drift is the sibling of timezone drift. The UI may be formatting dates, numbers, currencies, and sort order based on locale settings that differ between laptop and container.

What to verify:

  • LANG, LC_ALL, and related locale variables
  • Whether the image actually generated the locale you expect
  • Whether the browser session uses the intended language or region

A minimal image may boot with C.UTF-8 or another fallback locale. That is valid, but it is not equivalent to en_US.UTF-8 or de_DE.UTF-8. If your assertions depend on localized strings or number formatting, the locale must be explicit.

A useful diagnostic is to print formatting from the same code path the test uses. If the app uses the browser’s locale, confirm that the browser was launched with the correct language preferences rather than assuming the container will supply them.

Step 5: Look for missing system packages, not just missing app dependencies

Browser tests in Docker depend on native packages outside your application stack. If you use Chromium, Chrome, Firefox, or WebKit in a container, the browser needs shared libraries, certificates, sandbox support, and other OS-level pieces.

When the browser will not launch, or the process dies early, inspect the error message for missing .so libraries or sandbox failures. This is where “headless browser system packages” matter more than test code.

What to verify in a failing container

  • Browser binary exists and matches the test framework’s expectation
  • Shared libraries required by the browser are installed
  • ca-certificates is present if the test reaches HTTPS endpoints
  • The image has the packages needed for audio, graphics, fonts, and sandboxing, if your browser or framework requires them

If you are using Playwright, its installation and Docker guidance explicitly documents the need for browser dependencies and provides image and package guidance for Linux containers (Playwright docs). Selenium and Cypress have similar setup concerns, but the package lists differ by browser and image strategy.

A useful debugging habit

Dump the browser startup log on every CI failure. Many environment issues are visible in the launch command or in a missing-library error long before the test’s own assertion runs.

Step 6: Compare image layers, not just Dockerfiles

Two Dockerfiles can look similar and still produce different runtime behavior because the base image, install order, and cache state differ. When a test fails only in Docker, inspect the built image, not only the source file.

Recommended checks:

  • Pin the base image by digest for the failing branch or release candidate
  • Record the browser version in CI output
  • Compare package lists between the known-good and failing image
  • Rebuild without cache once to eliminate stale layers

A useful pattern is to make the CI job print a compact environment fingerprint:

cat /etc/os-release
node -v || true
npm -v || true
google-chrome --version || chromium --version || true
firefox --version || true
dpkg -l | grep -E 'font|locale|tzdata|libnss|libatk|libx11' || true

You are not trying to list every package. You are looking for obvious drift in fonts, timezone data, browser libraries, and security-related packages.

Step 7: Reproduce inside the exact container image

If CI uses a pinned image, your local reproduction should use the same image digest or at least the same tag plus package set. Do not debug with a neighboring image that “looks close enough.”

A practical reproduction loop is:

  1. Pull the exact CI image
  2. Run the test command inside it
  3. Mount only the code, not your host browser or host fonts
  4. Capture logs, screenshots, and browser stderr

Example:

docker run --rm -it \
  -e TZ=UTC \
  -e LANG=en_US.UTF-8 \
  -v "$PWD":/work \
  -w /work \
  your-image:tag \
  npm test

If the test passes only when mounted from your host, the host environment is leaking into the run, usually through fonts, cached browser state, or local config files.

A repeatable checklist for Docker-only browser failures

Use this order when a browser test fails only in Docker:

  1. Confirm the same test passes outside Docker
  2. Run the exact container locally
  3. Print OS release, browser version, locale, timezone, and font availability
  4. Check for missing shared libraries or browser launch errors
  5. Verify font packages and fc-match output
  6. Set timezone explicitly, do not rely on defaults
  7. Align locale and language settings across app, test, and container
  8. Pin the base image and browser version
  9. Rebuild the image without cache once
  10. Re-run the failing test with verbose logs and artifacts

That sequence keeps you from chasing test code when the environment is the real variable.

When it is probably an app bug instead

Not every Docker-only failure is environmental. Treat it as a likely app defect if:

  • The same image fails consistently across local Docker and CI
  • Fonts, timezone, locale, and browser packages are already pinned and verified
  • The failure reproduces in a non-headless browser on the same container
  • The app logs show the same backend error regardless of environment

If the failure remains after you have aligned the container, the browser, and the locale/time settings, then move back to product behavior, state management, or selector logic.

Short version

When browser tests fail only in Docker, start with the container, not the assertion. Fonts change layout, timezone changes date behavior, locale changes formatting, and missing system packages can stop the browser before the test starts. The fastest path is a structured environment diff, then a pinned reproduction in the exact CI image.

FAQ

Why do browser tests fail only in Docker when they pass locally?

Usually because the container does not match your desktop environment. Fonts, timezone, locale, browser version, and system libraries are the first things to compare.

How do I check for missing fonts in Docker browser tests?

Run fc-match and fc-cache -fv inside the container, then verify that the expected font packages are installed and indexed.

What causes timezone issues in browser automation?

The container may default to UTC or inherit a different timezone than the developer machine. Date-sensitive assertions, relative timestamps, and midnight boundary checks are the most affected.

What are headless browser system packages?

They are the native OS libraries and support packages a browser needs to launch and render correctly in Linux containers, including font, graphics, sandbox, and certificate dependencies.

Should I use UTC for all browser tests?

Use UTC for tests that do not depend on a user-facing local timezone. For locale-sensitive flows, set the timezone explicitly so the scenario matches the intended user context.

How do I know if the failure is in the app or the container?

Reproduce the test in the exact Docker image, then compare logs, fonts, locale, timezone, and browser startup output. If the failure disappears after environment alignment, it was likely container-related.