July 9, 2026
Why Browser Tests Pass Locally but Fail in Docker: A Beginner-Friendly Debugging Guide
Learn why browser tests fail in Docker even when they pass locally, plus a practical troubleshooting order for environment drift, browser dependencies, timing, and CI container issues.
Browser tests that pass on a laptop and fail inside Docker are one of the most common sources of confusion in test automation. The code may be the same, the test runner may be the same, and yet the result changes the moment the tests run in a container. That gap is usually not random. It is a sign of environment drift, the differences between your local machine and the containerized browser environment that affect timing, rendering, networking, file access, fonts, and browser startup.
For QA managers, SDETs, and DevOps teams, the useful question is not just “why did it fail?” but “what changed between local and Docker that could make this test flaky or deterministic?” Once you approach the problem in that order, the debugging process becomes much more manageable.
If a browser test behaves differently in Docker, assume the container is telling you something about hidden dependencies in the test, the app, or the environment.
This guide walks through the most common container-specific causes of local vs docker test failures, then gives you a practical troubleshooting order you can use in real projects. The examples focus on browser-based automation, including Playwright, Selenium, and similar frameworks, but the same ideas apply to most test stacks.
Why Docker changes browser test behavior
Docker containers are isolated Linux environments with their own filesystem, libraries, process limits, network stack, and runtime configuration. That isolation is what makes them useful in CI, but it also means they are not a perfect copy of your laptop. Even when the application code is identical, the browser and test runner may be seeing a different world.
A few common differences matter a lot:
- The browser may run headless in Docker, but headed locally.
- The container may have fewer fonts, locale packages, or system libraries.
- CPU and memory limits can make timing different.
- DNS and networking can behave differently from your host machine.
- File paths, permissions, and mounted volumes can behave differently.
- The browser version in the container may not match your local installation.
These differences are why browser tests fail in Docker even when they look stable locally. The browser is not just clicking buttons, it is depending on the operating system, rendering stack, and runtime assumptions around it.
If you want a broad background on automation and CI concepts, the general ideas are covered in test automation and continuous integration. For Docker itself, the official documentation is the best reference: Docker Docs.
The most common causes, in plain English
1. The browser is missing system dependencies
This is one of the first things to check because it often causes immediate startup failures or strange rendering behavior. Local machines usually have a richer desktop environment than slim container images. In Docker, browser automation often needs extra libraries for graphics, sound, sandboxing, and font rendering.
Typical symptoms include:
- The browser does not launch
- Tests crash before the first page loads
- Screenshots show blank pages or broken text
- Chromium-based browsers complain about missing libraries
A common mistake is to use a minimal base image and assume the browser package alone is enough. It usually is not. Chromium, Firefox, and WebKit each have their own dependency requirements, and they can vary by image and Linux distribution.
For teams using Playwright, the official browser installation instructions help here because they include the system dependencies needed by supported images and distros. For Selenium, the issue is often in the browser image, the grid node, or the custom Dockerfile used to build the test environment.
2. The browser version differs from local
A local test may pass because it is running against Chrome 127, while the container is using Chrome 123 or a different patch version of Chromium. Most modern browser automation frameworks are tolerant of small changes, but UI behavior, accessibility tree details, and rendering quirks can shift across versions.
This shows up as:
- Selectors working locally but not in Docker
- Layout-dependent assertions failing
- Screenshot diffs that look minor but break tests
- Features available in one environment but not another
Version drift is especially painful when a developer updates their local browser automatically, but CI images lag behind. The fix is not just “update everything,” but “pin and align the browser versions deliberately.”
3. Headless mode behaves differently than headed mode
Many browser tests run headless in containers. Headless mode is practical, but it is not always identical to a real desktop session. Differences can include viewport size, GPU availability, font fallback, animation timing, and how the browser handles some scrolling and focus behavior.
A test may pass locally because a human-visible browser window gives the page time to settle, while the headless container finishes a frame earlier and the assertion runs too soon. Or the local browser may render a responsive layout differently due to a larger default window size.
When a bug appears only in Docker, check whether the headless configuration is changing the page enough to matter. For example, a menu might collapse into a mobile layout when the container’s viewport is smaller than expected.
4. The test is too dependent on timing
Timing issues are the heart of many local vs docker test failures. Containerized browser testing can be slower because the CPU is shared, startup overhead is higher, and the browser has fewer resources. A test that accidentally passes on a fast laptop may fail in Docker because it does not wait for the application to become ready.
This is the classic symptom pattern:
sleep(2)works locally, fails in CI- Assertions happen before API data finishes loading
- Page transitions take longer in Docker
- Flaky tests appear only on busy build agents
Hard-coded sleeps are usually the wrong fix. Prefer explicit waits for visible conditions, network idle only when appropriate, and assertions that reflect the user’s actual path through the UI.
5. Fonts and rendering are different
Fonts sound trivial until a test relies on text layout, exact screenshots, or element geometry. Docker images often have a much smaller font set than a developer laptop. If your application falls back to another font family, line breaks, button sizes, and even clipping can change.
This matters for:
- Visual regression tests
- Tests that locate elements based on rendered text size or position
- PDF export checks
- Screenshot-based approvals
If a UI test checks exact pixels, font differences can create false failures. Even when the app is functionally correct, the output can look different enough to fail image comparison.
6. The container does not have enough resources
Browser tests are resource-hungry. A tiny container running a modern browser may hit memory pressure, crash tabs, or slow down enough to expose hidden race conditions. On a laptop, the browser may have many gigabytes available. In Docker, the container may be constrained to a fraction of that.
Watch for:
- Renderer crashes
- Out-of-memory exits
- Slow startup or navigation
- Tests that fail only under parallel execution
Resource limits are not always the primary cause, but they often make a weak test fail faster. That is useful if you treat the failure as a signal, not as an annoyance to suppress.
7. Networking and DNS behave differently
Your test may be calling a local app, mock server, or fixture service. In Docker, “localhost” means the container itself, not your host machine. That alone can break browser tests that worked locally.
Common networking mistakes include:
- Using
localhostin the app URL when the app runs elsewhere - Expecting a mock API running on the host to be reachable from the container
- DNS resolving differently in CI than on the laptop
- TLS certificates not trusted inside the image
This is especially common in local development with compose files or ad hoc test containers. If the browser cannot reach the app or API, the test may fail with a timeout that looks like a UI problem but is actually a connectivity issue.
8. File paths and permissions are different
Containers often run as a different user than your host account. The browser may need to write screenshots, videos, trace files, downloads, or temporary profiles. If the mounted volume or output directory is not writable, tests may fail in ways that look unrelated to filesystem permissions.
Examples include:
- Screenshot capture fails
- Download assertions fail because files never appear
- Browser profile cannot be created
- Test artifacts are missing after the run
Check who owns the directory, which user the container runs as, and whether mounted paths are consistent with what the framework expects.
A troubleshooting order that actually works
When browser tests fail in Docker, the fastest path is usually not to inspect every line of the test. Start with the environment, then narrow down to the app, then the test logic.
Step 1: Reproduce the failure with one test, one browser, one container
Reduce the problem. Run the smallest failing test case in the same container image used by CI. If the test suite is large, isolate the single failing spec.
This helps you answer three questions:
- Is the failure deterministic?
- Is it browser-specific?
- Is it image-specific or suite-specific?
If the issue disappears when you run one test, the problem may be resource contention or shared state across tests.
Step 2: Compare the local and Docker environments
Create a checklist of versions and runtime settings:
- Browser version
- Test runner version
- Node, Python, Java, or .NET runtime version
- Base image tag
- Environment variables
- Viewport size and headless/headed mode
- CPU and memory limits
Even a small difference can explain a failure. Teams often discover that local developers are testing a newer browser or a different environment variable than CI.
A simple way to inspect the environment inside a container is to print versions at startup:
docker run --rm your-test-image sh -lc 'node -v && google-chrome --version && printenv | sort | head -50'
Step 3: Check whether the app itself is available from inside the container
A browser test failure may be a connectivity failure in disguise. From inside the container, verify that the app URL is reachable and returns the expected status.
docker exec -it test-runner sh -lc 'curl -I http://host.docker.internal:3000 || true'
If you are on Linux or another environment where host.docker.internal is not available by default, the networking setup needs attention. The key point is that the browser container must be able to reach the application under test using the correct network path.
Step 4: Disable hidden complexity
Temporarily reduce the variables that can break a browser test:
- Run a single worker
- Disable parallelism
- Use one viewport size
- Disable video or trace capture if it changes timing
- Turn off unnecessary browser extensions or custom startup flags
This is not the final configuration, but it helps you determine whether the failure is caused by concurrency, startup overhead, or a specific browser setting.
Step 5: Capture artifacts that show what the browser saw
Screenshots, videos, logs, and traces are essential in containerized browser testing. Without them, you are guessing.
For Playwright, a minimal debugging setup might look like this:
import { test, expect } from '@playwright/test';
test('home page loads', async ({ page }) => {
await page.goto('http://app:3000');
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
});
Then enable traces in the test runner configuration so you can inspect timing and DOM state after a failure. For Selenium, you can get similar value from browser logs, screenshots, and explicit waits around critical steps.
Practical fixes for the most frequent Docker-only failures
Match the browser and image versions
Use pinned image tags and consistent browser versions across local and CI where possible. Avoid floating tags that change unexpectedly. If your team uses a custom Dockerfile, document the browser package source and upgrade policy.
A good rule is to treat browser version upgrades like dependency upgrades, not incidental image refreshes.
Make the viewport explicit
If layout matters, set the viewport deliberately instead of relying on defaults.
import { test } from '@playwright/test';
test.use({ viewport: { width: 1440, height: 900 } });
This prevents container defaults from pushing the app into a different responsive state.
Replace sleeps with condition-based waits
Bad timing assumptions are one of the most common reasons browser tests fail in Docker. If the test needs to wait for a spinner to disappear, wait for the spinner, not for a guessed number of seconds.
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
WebDriverWait(driver, 10).until( EC.invisibility_of_element_located((By.CSS_SELECTOR, “.loading-spinner”)) )
This is more resilient than sleeping and hoping the app loads in time.
Provision the right dependencies in the image
If browser startup fails, inspect the image build and install the documented system dependencies for the browser and runtime. Base images designed for automation are often safer than hand-rolled minimal images if your team does not want to maintain low-level packages.
Increase observability
A test that fails in Docker should tell you where it failed. Add logging around navigation, API calls, and critical assertions. Store screenshots and traces as build artifacts. The goal is to turn a vague timeout into a specific state transition that did not happen.
Keep test data and environment state isolated
Shared state is a classic source of flaky browser tests. In containerized test runs, isolation becomes even more important because the suite may run in parallel or on ephemeral build agents.
Check for:
- Reused user accounts
- Sticky session state
- Cached API responses
- Shared download directories
- Reused browser profiles
If one test affects another, Docker can make the problem more visible because the environment is cleaner and the timing is tighter.
A simple decision tree for QA teams
If you need a quick triage path, use this order:
- Does the browser start in Docker? If not, check dependencies and image setup.
- Can the browser reach the app? If not, check network, hostnames, and ports.
- Does the page load but the test fail? Check selectors, timing, and viewport.
- Does it fail only in headless mode? Check rendering, font differences, and layout assumptions.
- Does it fail only in parallel or under load? Check resource limits and shared state.
- Does it fail only after a browser update? Check version drift and compatibility.
That sequence is better than jumping straight into the test code. It narrows the search area and helps you avoid fixing the wrong layer.
What this means for tool selection and team process
For QA managers and founders choosing testing tools, Docker failures are not just a technical nuisance. They are a signal about whether the tool and the team can handle environment drift cleanly.
When evaluating browser automation tools, look for support in these areas:
- Clear container setup instructions
- Reliable artifact capture, such as screenshots and traces
- Good support for explicit waits and stable selectors
- Browser version management and reproducible execution
- Straightforward CI integration
- Debugging output that makes local vs Docker test failures easier to diagnose
A team can absolutely run robust browser automation in Docker, but it requires disciplined environment management. If your testing approach depends heavily on visual exactness, shared state, or timing guesses, Docker will expose those weaknesses quickly.
The best browser test suite is not the one that passes once, it is the one that fails for a reason you can explain.
Common mistakes to avoid
Treating Docker as a black box
If your only debugging tool is “rerun the pipeline,” you will spend a lot of time guessing. Treat the container as part of the test system, not just a delivery wrapper.
Assuming localhost means the same thing everywhere
It does not. In Docker, localhost usually refers to the container itself. This single assumption causes many local vs docker test failures.
Using fixed waits as the primary synchronization method
Sleep-based tests are fragile. They may mask a real performance issue locally and then fail in Docker when the environment is slower.
Ignoring image updates
A browser version or dependency change inside the image can change behavior even when no test code changed. Track image changes like you track application changes.
Skipping artifact collection
Without screenshots, videos, logs, or traces, browser failures become opaque. In Docker, opaque failures are expensive.
A minimal CI example with Docker
If you are wiring a browser suite into CI, keep the job simple and explicit. This kind of structure is easier to debug than a large, hidden template.
name: browser-tests
on: [push, pull_request]
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build test image run: docker build -t browser-tests . - name: Run tests run: docker run –rm browser-tests
The exact implementation will vary, but the principle is the same, build the same image you test with, run the same browser stack, and capture enough output to debug failures later.
Final takeaway
When browser tests pass locally but fail in Docker, the problem is usually not mysterious. It is usually one of a small set of container-specific differences, missing dependencies, browser version drift, headless rendering differences, timing assumptions, networking issues, permissions, or resource limits.
The fastest way to fix these problems is to stop asking, “Why is Docker breaking my test?” and start asking, “What does Docker reveal about this test that my laptop hides?” That shift leads to better debugging, more reliable automation, and fewer surprises in CI.
If your team standardizes the browser image, makes waits explicit, captures good artifacts, and checks networking first, most browser tests fail in Docker issues become straightforward to diagnose instead of mysterious.