How to Fix Missing Fonts in Selenium Headless Chrome Screenshots on Linux
Diagnose font substitution in headless Chrome with Fontconfig, install the right Linux font packages, and make Selenium screenshots reproducible.
To fix missing or substituted fonts in Selenium headless Chrome on Linux, install the required font family or a suitable substitute in the same host, container, or CI image that runs Chrome. Use fc-list to inspect installed fonts and fc-match "Family Name" to see what Fontconfig resolves for the page’s CSS family. Then start a fresh Chrome session and capture again.
Headless Chrome uses the fonts available to its Linux environment and that environment’s font matching configuration. Headless mode does not provide a separate font library. If the expected family or required glyphs are unavailable, fallback matching can change text shapes, widths, and line wrapping. [Chrome Headless mode]
1. Confirm the screenshot has a font problem
Look for text wrapping at different words, visibly different letter shapes or widths, and missing characters rendered as boxes or replacement glyphs. These clues suggest font substitution or incomplete glyph coverage, but they are not proof on their own. Compare the page’s actual CSS font-family stack with the fonts available in the environment that launched Chrome.
If only certain characters are wrong, suspect a script or symbol coverage gap. If all text looks different, check whether the requested family is installed and whether Fontconfig resolves it as expected. A webfont that has not finished loading can also make a screenshot look like a system-font problem; investigate that separately if the system font match is correct.
2. Check fonts in the exact Linux runtime
Run these commands inside the Selenium container or VM, or on the host that runs Chrome. Running them on your workstation does not tell you what is installed in a remote CI runner.
# List fonts Fontconfig can see
fc-list
# Ask Fontconfig which font it resolves for the family used by the page
fc-match "Arial"
fc-match "Inter"
# Inspect available Fontconfig commands
fc-list --help
fc-match --help
Replace the sample family names with the family at the start of the page’s CSS stack. Fontconfig locates fonts and selects matches for applications; fc-list lists fonts and fc-match tests matching rules. [Debian Fontconfig package]
If fc-match returns a fallback, that does not necessarily mean the page is broken: the requested family may be absent, or its name may not match the installed family. Check the selected font against the intended family and confirm the CSS really requests that family.
3. Install fonts in a Debian-based Selenium image
For pages that expect common Arial, Times New Roman, or Courier New metrics, Debian’s fonts-liberation package provides metric-compatible Liberation serif, sans-serif, and monospaced families. It is a substitute, not the identical proprietary typeface. [Debian fonts-liberation]
For broader Unicode coverage, select Noto packages based on the scripts and symbols the page needs. Debian packages include fonts-noto-core, fonts-noto-cjk, and fonts-noto-color-emoji; one package should not be assumed to cover every script or symbol. [Debian font packages]
FROM selenium/standalone-chrome:latest
USER root
RUN apt-get update && apt-get install -y --no-install-recommends \
fontconfig \
fonts-liberation \
fonts-noto-core \
&& rm -rf /var/lib/apt/lists/*
# Add only if the rendered page needs these scripts or symbols:
# RUN apt-get update && apt-get install -y --no-install-recommends \
# fonts-noto-cjk fonts-noto-color-emoji \
# && rm -rf /var/lib/apt/lists/*
USER seluser
This Dockerfile illustrates the installation step for a Debian-based image; confirm the base image’s user and package manager before using it. Package names vary across distributions and releases. Rebuild and redeploy the image after changing packages. If the environment requires it, refresh Fontconfig’s cache with fc-cache -f, then start a new Chrome process so it sees the installed fonts.
Choose packages by what the page renders, not by the fact that Chrome is headless. If the page uses a proprietary font, use a licensed copy where permitted, or select a substitute and accept that glyph shapes may differ even when metrics are compatible. Review font licensing before bundling font files into an image.
4. Recheck font matching and capture in Selenium
After rebuilding the image, verify the requested family again and launch a fresh WebDriver session. Keep the viewport and the condition used to decide that the page is ready consistent between captures.
# Run inside the rebuilt container
fc-match "Arial"
fc-list | sort | head -40
Example Python Selenium capture with a fixed viewport:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_argument("--headless")
options.add_argument("--window-size=1440,1000")
driver = webdriver.Chrome(options=options)
try:
driver.set_window_size(1440, 1000)
driver.get("https://example.com")
driver.save_screenshot("page.png")
finally:
driver.quit()
Replace the example URL with the page you need to capture. This minimal example waits for navigation to return; pages that load fonts or content asynchronously may need a page-specific readiness check. Do not treat a fixed sleep as a universal guarantee that a webfont has loaded.
Selenium’s Chrome options let you pass browser arguments. Keep Chrome and ChromeDriver aligned at the major-version level as Selenium documents. Browser flags and viewport settings control browser or capture conditions; they do not install fonts. [Selenium Chrome browser options] [Chrome Headless command-line reference]
5. Make screenshots reproducible in CI and Docker
- Install font packages in the same image or machine that starts Chrome, including in every CI job that captures screenshots.
- Pin the image and font package set used for a capture workflow so a rebuild does not silently change the available fonts.
- Keep Chrome and ChromeDriver major versions aligned.
- Hold the viewport, device scale, page URL, and page-ready condition constant when comparing screenshots.
- Record the relevant font package set and the output of
fc-matchwhen investigating a difference between local and CI captures.
Chrome’s current Headless mode shares the regular browser implementation. The older separate Headless implementation was superseded by unified Headless in Chrome 112; since Chrome 132, the old implementation is available only as the standalone chrome-headless-shell binary. Check the version actually deployed if you are comparing older environments. [Chrome Headless mode]
Or skip the browser setup
If you need a screenshot without maintaining Chrome, ChromeDriver, and Linux font packages, ScreenshotNeo is a website screenshot API and MCP server. Its API takes a URL and returns an image or PDF; see the ScreenshotNeo 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}`);
await Bun.write('shot.webp', res);
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Create a free account and get 1,000 screenshots a month with no card.
Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
fc-match resolves to an unexpected family |
The requested font is missing, or Fontconfig is matching a fallback. | Install the needed family or an appropriate substitute in the runtime image; rerun fc-match. |
| Latin text looks right, but CJK characters are boxes or wrong | The installed fonts do not cover the characters or script. | Add the relevant script package, such as Debian’s fonts-noto-cjk, then recheck the match and capture. |
| Emoji are missing or monochrome | The available font set may not provide the required emoji glyphs or presentation. | Check the page’s actual emoji requirements and consider the distribution’s emoji font package, such as Debian’s fonts-noto-color-emoji. |
| It works locally but not in CI | The local machine and CI runner have different images, package sets, font directories, or cache visibility. | Run fc-list and fc-match inside the CI runtime; install fonts there and rebuild the image. |
| The font was installed, but Chrome still uses the old result | The browser may have been started before installation, or Fontconfig’s cache may need refreshing. | Refresh the cache if needed and start a new Chrome/WebDriver session. |
| Fontconfig resolves the expected family but the screenshot still differs | The mismatch may be in webfont loading, CSS, readiness timing, viewport, device scale, or Chrome version. | Check those conditions separately and keep them fixed across comparison captures. |
| Text width improves but letter shapes still differ | A metric-compatible substitute has similar layout metrics but is not the original typeface. | Use the licensed original font if its exact appearance is required; otherwise accept the substitute’s shape differences. |
| Package installation fails | The image may not be Debian-based, package indexes may be stale, or package names differ for that release. | Check the distribution and release, update its package index, and use the package names available there. |
Performance, reliability, and cost
Adding fonts changes the browser environment’s installed resources; package size and installation time depend on the chosen packages and distribution, so select only the script coverage the page requires. Installing fonts into a reusable CI image avoids repeating package installation for each capture. Pinning the image and package set makes visual comparisons more reliable, while a fresh browser process after a font change avoids relying on a session that started before the change.
Font installation itself does not require a paid screenshot service. The operational cost is maintaining the image and its dependencies. If screenshot volume or browser maintenance is the concern, ScreenshotNeo’s free tier includes 1,000 shots a month; its paid plans start at $5 for 3,000. The API response includes page-verdict and billing headers, and cache hits are not billed.
Frequently asked questions
Why does Chrome use a different font in headless mode?
Headless Chrome uses fonts and font matching available in its Linux runtime. Differences between a desktop and a container commonly come from different installed font sets or matching configuration, rather than a separate headless font library.
How do I install fonts in a Selenium Docker image?
Install the distribution’s font packages in the image that runs Chrome, rebuild it, verify with fc-match, and start a fresh WebDriver session. The Debian example above uses Liberation and Noto packages.
How can I check which font Chrome is using?
Use fc-match "Family Name" inside Chrome’s runtime to see Fontconfig’s match for that family, and fc-list to inventory visible fonts.
Does --window-size fix missing fonts?
No. It sets the capture viewport, which helps make layout comparisons consistent. Fonts must be present and resolvable in the Linux environment that runs Chrome.
Will Liberation look exactly like Arial?
No. Debian describes Liberation fonts as having the same metrics as common Times, Arial, and Courier families. Metric compatibility can help stabilize layout, but the glyph shapes are not identical.


