Why Browser Tests Break in Docker When Fonts or Locale Change the Layout
By David Frei · October 11, 2026
A practical guide to debugging browser tests that fail only in Docker because of missing fonts, locale packs, or font fallback differences, with reproducible checks and fixes.
If a browser test passes on your laptop and fails only in Docker, I would look at fonts and locale before I blame timing. Text metrics change when the container lacks the same fonts, fontconfig data, or locale packages that exist on a developer machine. That can shift line wrapping, button widths, element positions, and even screenshot diffs.
The useful distinction is this: browser rendering differences are layout changes caused by the environment, while test logic failures are caused by incorrect selectors, waits, or assertions. Docker can trigger both, but font and locale issues usually show up as the same app behaving differently, not as a broken automation framework.
If the DOM is the same but the pixels move, suspect typography first.
What usually changes inside Docker
Three environment details account for a surprising amount of layout drift:
- Missing fonts, so the browser falls back to a different family.
- Locale packs or ICU data, so text expands, truncates, or formats differently.
- Font fallback order, so a glyph comes from a different font than on the host.
The browser does not care that your app uses font-family: Inter, Arial, sans-serif if Inter is absent in the container. It will substitute the first available font that can render the text. That changes character widths and line breaks, especially with wide languages, accented characters, emoji, or CJK text.
Locale affects more than translated strings. Date formats, currency formats, pluralization, and number grouping can all alter text length. A label that fits in English may wrap in German or French. A date like 9/1/2026 may become 01.09.2026 or 9 Sept 2026, which is enough to push a flexbox layout over the edge.
Prove that it is a rendering problem, not a flaky assertion
Before changing the test, prove the environment is the variable. I use a short checklist.
1) Compare screenshots from host and container
Take a full-page screenshot from the same test step in both environments. If the failure is layout-related, you should see one of these patterns:
- text wraps onto an extra line
- a button becomes partially hidden
- an overlay moves and intercepts clicks
- a grid cell changes height
- a visual diff appears even though locators are correct
In Playwright, that can be as simple as:
import { test } from '@playwright/test';
test('capture page state', async ({ page }) => {
await page.goto('https://example.com');
await page.screenshot({ path: 'page.png', fullPage: true });
});
In Selenium, the equivalent is also straightforward, although you usually manage the screenshot file yourself.
2) Inspect the computed font on the failing element
The browser may request one font and render another. Check what actually won the cascade and fallback chain.
const styles = await page.$eval('.price', el => {
const cs = getComputedStyle(el as HTMLElement);
return {
fontFamily: cs.fontFamily,
fontSize: cs.fontSize,
lineHeight: cs.lineHeight,
width: cs.width,
height: cs.height,
text: (el as HTMLElement).textContent,
};
});
console.log(styles);
If fontFamily is the same in both places but the rendered width differs, that usually means the named font is not actually present in one environment, or the fallback differs.
3) Check the browser’s viewport and device scale factor
Font issues sometimes get blamed for what is really a viewport mismatch. Make sure the container uses the same browser size and scale factor as the local run. A text block that fits at 1440 px wide may wrap at 1280 px.
4) Record the exact text that rendered
If you use localization, confirm the string itself. The layout problem may be caused by a different translated string, not the font. I like logging the visible text alongside the computed styles. That makes diffs obvious.
Inspect the container from the inside
Once you suspect the environment, inspect the image directly. The goal is to answer four questions:
- which fonts are installed
- which locale packages are installed
- what fontconfig sees
- whether the browser image matches the OS image assumptions
A few shell commands go a long way:
bash fc-match sans fc-match serif fc-list | head -n 20 locale cat /etc/os-release
fc-match tells you what fontconfig will choose for a family. fc-list shows which fonts are actually present. locale shows whether the container has locale variables set. If your app expects UTF-8 but the container is using a minimal locale setup, string handling and rendering can get weird fast.
For Ubuntu or Debian-based images, I also check whether the locale archive exists and whether the relevant package is installed. On stripped-down images, the browser may run fine while rendering differently because the typography stack is incomplete.
Stabilize the environment before loosening the test
My default rule is simple: fix the environment first, then loosen assertions only if the layout is intentionally variable.
Install the fonts your app actually needs
If your design system specifies a web font, make sure the CI image can render it or a close substitute. For example, if your product uses common sans-serif text and icon glyphs, install the font packages your UI depends on rather than assuming the base image has them.
For containerized browser runs, start from a browser image or a maintained base image that already includes the browser dependencies, then add only what your application needs. Docker’s documentation is the right place to start when you are deciding how much to build yourself versus inherit from an existing image: Docker docs.
A lightweight example for Debian-based images:
dockerfile FROM mcr.microsoft.com/playwright:v1.49.0-jammy
RUN apt-get update && apt-get install -y –no-install-recommends
fonts-dejavu
fonts-liberation
locales
&& rm -rf /var/lib/apt/lists/*
ENV LANG=en_US.UTF-8 ENV LC_ALL=en_US.UTF-8
The exact packages depend on your app, but the pattern matters more than the package names: keep the browser base stable, then add fonts and locale support explicitly.
Set the locale deliberately
If your tests assert on user-visible text, set the locale in the container and in the application under test. Do not let defaults drift between developer laptops and CI. A small change in LANG can alter date and number formatting enough to break a layout assertion.
Use one browser image per browser line
Mixing a local Chrome binary with a random base image makes font problems harder to reproduce. For Playwright, the official docs document the supported installation and runtime model clearly, which is useful when you want the browser, system dependencies, and test runner to stay aligned: Playwright docs.
Decide whether to install fonts or change the assertion
Not every layout change is a bug. Some text is supposed to grow, localize, or wrap. The question is whether your assertion is testing behavior or accident.
Install or standardize fonts when the text should be stable
Choose the environment fix when the UI depends on consistent text metrics, such as:
- buttons sized to fit a known label
- screenshots used as a regression signal
- pixel-sensitive drag-and-drop or overlay positioning
- reports or invoices where line wrapping matters
If the app is expected to look the same across CI and production, then the container should mirror production closely enough to make typography deterministic.
Loosen the assertion when the layout is intentionally flexible
Relax the test when the exact pixels are not the feature being verified. Better assertions include:
- checking that the button is visible and clickable instead of checking its exact width
- asserting the text content rather than a screenshot
- waiting for a modal to be displayed instead of expecting a specific y-position
- comparing semantic conditions, such as ARIA state or enabled status
A test that fails because a translated label is 12 pixels wider is often overfitted. The layout may be acceptable, while the assertion is too strict.
The right question is not “why did the pixels change?”, it is “was the thing I asserted actually part of the contract?”
A compact decision table
| Symptom in Docker | Likely cause | Best first fix |
|---|---|---|
| Same DOM, different screenshot | Missing or substituted font | Install the expected font family |
| Text wraps only in CI | Font width or locale string length changed | Check computed styles and locale, then adjust layout or container image |
| Dates, currencies, or numbers format differently | Locale mismatch | Set LANG, LC_ALL, and app locale explicitly |
| Click intercepted after a text change | Layout reflow from font fallback | Stabilize fonts or avoid position-sensitive clicks |
| Visual diff with no DOM diff | Font rendering or hinting difference | Align OS image and font packages |
A reproducible debug workflow I trust
When I inherit one of these failures, I move in this order:
- Capture screenshots from local and CI.
- Log computed styles for the failing element.
- Check
fc-matchandfc-listinside the container. - Verify locale variables and browser viewport settings.
- Compare the base image with the local environment.
- Decide whether the test should assert layout or behavior.
That sequence keeps me from making a test pass by accident. It also avoids the common trap of rewriting assertions before the environment is understood.
Common failure modes that waste time
“It must be Playwright” or “It must be Selenium”
The automation framework is usually not the root cause. Both Playwright and Selenium can expose the same underlying rendering mismatch because both rely on the browser and the OS font stack. If the browser is drawing different glyphs, the framework is just the messenger.
“We already installed Chrome”
Installing the browser binary is not the same as installing the fonts and locale packages it depends on. A minimal image can launch Chrome successfully and still render differently from your workstation.
“The app is localized, so screenshot diffs are expected”
Maybe, but that is not a reason to accept unstable tests. It is a reason to choose better assertions or to pin the test locale intentionally. If a single locale is under test, lock it down. If multiple locales are supported, test the text and layout rules separately.
When to move from debugging to policy
If this failure repeats across multiple tests, treat it as a build-image problem, not a test problem. Add a policy for browser CI images:
- base image version pinned
- font packages listed explicitly
- locale variables set in one place
- screenshot and viewport settings standardized
- a small smoke test that exercises a text-heavy page
That policy pays off because typography regressions are easier to prevent than to triage.
Bottom line
If browser tests fail only in Docker after a font or locale change, prove the rendering difference first, then fix the environment before touching the assertion. In my view, the fastest reliable path is:
- compare screenshots
- inspect computed font styles
- verify installed fonts and locale data inside the container
- standardize the image
- relax only the assertions that were overspecified
That is usually the shortest path to making the failure go away for the right reason.
FAQ
Why do browser tests pass locally but fail in Docker?
Usually because the container is missing fonts, locale data, or the same viewport settings as the local machine. The browser renders the page correctly according to its available environment, which is not always the environment you expected.
How do I check which font the browser is actually using?
Inspect the computed style in the page and compare it with fc-match inside the container. The computed font-family tells you what CSS requested, while fontconfig tells you what the OS can actually supply.
Should I install custom fonts in CI?
Yes, if your layout depends on stable typography or your app uses a brand font that affects width and wrapping. If the exact font is not important, standardize on a reliable fallback set and test behavior instead of pixel-perfect layout.
Do locale packs affect browser test layout?
Yes. Locale changes can alter translated text length, date formatting, number formatting, and pluralization. Any of those can change line wrapping or element size.
Is this more likely in Playwright or Selenium?
Neither framework is the main cause. The browser and the container image drive the rendering differences. Playwright and Selenium both surface the issue because they automate the same browser stack.