How to Download Fonts in Dockerized Puppeteer
Install the right fonts in your Puppeteer image, verify rendering, and avoid missing glyphs in screenshots and PDFs.
Direct answer: Fonts used by Dockerized Puppeteer must be available inside the Linux environment where Chrome runs. Install distribution-appropriate font packages during the image build, or copy approved font files into the image. Then wait for web fonts before creating a PDF, and verify the exact scripts your pages contain.
The package names depend on your base distribution and the characters you need to render. Puppeteer’s troubleshooting guide gives charset-oriented examples such as IPA Gothic, WenQuanYi Zen Hei, Thai TLWG, KACST, and FreeFont; treat those as examples to adapt, not as a universal package list. Read the troubleshooting guidance.
1. Choose your Docker base image
Puppeteer documents an official image that includes Chrome for Testing, required dependencies, and Puppeteer. If you use another base image, start from the project’s Dockerfile and keep the browser, Puppeteer, Node, and system dependencies aligned. The Docker guide retrieved for Puppeteer 25.12.0 documents Node 22.12+ as the system requirement. Docker guide · System requirements.
| Route | Use it when | Trade-off |
|---|---|---|
| Official Puppeteer image | You want Chrome and its dependencies maintained together. | Less control over the base image and installed packages. |
| Custom Node image | You need a specific OS, security baseline, or font inventory. | You own browser dependencies, package names, and upgrades. |
2. Install fonts during the image build
Installing at build time makes every container replica use the same font set. Do not download fonts from an untrusted URL at container startup. Pin packages or copy licensed files from a controlled build context.
Custom image with distribution packages
The following pattern is runnable on a Debian-family base after you confirm the package names in your selected repository. The names shown are representative charset packages called out by Puppeteer’s documentation; package availability can change.
FROM node:22-bookworm
ENV PUPPETEER_CACHE_DIR=/tmp/puppeteer-cache
WORKDIR /app
# Confirm these names for your Debian release before relying on them.
ARG FONT_PACKAGES="fonts-freefont-ttf fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst"
RUN apt-get update \\
&& apt-get install -y --no-install-recommends $FONT_PACKAGES \\
&& rm -rf /var/lib/apt/lists/*
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["node", "render.js"]
If a package is unavailable, remove it or replace it with the equivalent package for that distribution. A package name that works on Debian may not exist on Alpine, Ubuntu, or another base.
Copy approved font files into the image
For proprietary or organization-managed fonts, place the files in a controlled fonts/ directory and copy them into a system font directory.
FROM node:22-bookworm
WORKDIR /app
RUN mkdir -p /usr/local/share/fonts/company
COPY fonts/*.ttf /usr/local/share/fonts/company/
COPY fonts/*.otf /usr/local/share/fonts/company/
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["node", "render.js"]
Use font files you are licensed to redistribute. If your distribution provides a font-cache utility, run the distribution’s documented cache-refresh command; otherwise restart the container and verify discovery with the checks in the next section.
3. Verify that Chrome can see the fonts
Check the built image before debugging your page. A missing file in the image cannot be fixed by Puppeteer options.
docker build -t puppeteer-fonts .
docker run --rm -it puppeteer-fonts sh
# Inside the container, inspect installed files and directories.
find /usr/share/fonts /usr/local/share/fonts -type f 2>/dev/null | head -50
node --version
npx puppeteer --version
For a deterministic check, render a page containing Latin, CJK, Arabic, and Thai samples and compare the output against a known-good reference. Inspect the browser console for failed web-font requests and use CSS font-family fallbacks deliberately.
4. Wait for web fonts before screenshots and PDFs
System fonts are available as soon as Chrome starts. Fonts loaded by CSS or JavaScript may arrive later. Puppeteer’s PDF API waits for document.fonts.ready by default because PDFOptions.waitForFonts defaults to true. A background page may need page.bringToFront() first. PDF generation guide · PDFOptions API.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle0', timeout: 90000 });
await page.bringToFront();
await page.evaluate(async () => {
await document.fonts.ready;
});
await page.pdf({
path: 'output.pdf',
format: 'A4',
printBackground: true,
waitForFonts: true
});
} finally {
await browser.close();
}
})();
5. Handle CJK and other script coverage
A font that covers English may not contain Chinese, Japanese, Korean, Arabic, Thai, or emoji glyphs. Select packages based on the actual text and fallback rules in your CSS. Test mixed-script strings, punctuation, numerals, combining marks, and right-to-left text.
<style>
body {
font-family: "YourPrimaryFont", "Noto Sans", sans-serif;
}
.cjk {
font-family: "YourCjkFont", sans-serif;
}
</style>
English · 中文 · 日本語 · 한국어 · ไทย · العربية
When a glyph is absent, Chrome may show a tofu box or silently select a fallback. That is a font-coverage problem, not a Puppeteer screenshot bug.
6. Keep Chrome’s writable paths available
Chrome creates profile, configuration, and cache files at startup. A read-only container can fail before a page loads. Give the process writable locations, commonly under /tmp, or mount writable volumes according to your runtime policy.
const browser = await puppeteer.launch({
headless: true,
userDataDir: '/tmp/chrome-profile',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disk-cache-dir=/tmp/chrome-cache'
]
});
Do not disable the sandbox casually. If your deployment requires --no-sandbox, apply the container isolation controls required by your platform.
7. Control browser and Puppeteer installation
puppeteer downloads a compatible Chrome by default. puppeteer-core is for teams that manage the browser separately or connect to a remote browser. Configuration supports a cache directory, executable path, and skipped downloads. Installation guide · Configuration API.
// puppeteer.config.cjs
module.exports = {
cacheDirectory: '/tmp/puppeteer-cache'
};
Keep the browser version and Puppeteer version compatible. Rebuild the image when either changes, then rerun your font coverage fixtures.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Boxes or tofu glyphs | The required script is not covered by installed fonts. | Add a package or licensed font covering those characters; verify the CSS fallback chain. |
| PDF uses a different font than the screenshot | Web fonts were still loading or PDF generation started in a background tab. | Bring the page to the front, await document.fonts.ready, and keep waitForFonts: true. |
| Fonts work locally but not in Docker | The host fonts are not present in the image. | Install packages or copy files during docker build; inspect the image with find. |
| Package not found | Names differ between distributions or repository releases. | Search the selected distribution’s current repositories and replace the illustrative package name. |
| Browser exits at launch | Missing Chrome dependencies or a read-only profile/cache path. | Use the official image or its Dockerfile as a baseline and route profile/cache paths to writable locations. |
| Text is clipped or wraps differently | Fallback metrics differ, or the font loaded after layout. | Wait for fonts before measuring or capturing; set explicit font stacks and viewport dimensions. |
| Emoji render as monochrome or boxes | No suitable color-emoji font exists in the image. | Install a permitted emoji-capable font and test the target Chrome/Linux combination. |
9. A repeatable rendering test
const puppeteer = require('puppeteer');
const html = `<!doctype html>
Latin: Hello
Chinese: 中文
Japanese: 日本語
Korean: 한국어
Thai: ไทย
Arabic: العربية
`;
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'load' });
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'font-fixture.png', fullPage: true });
await page.pdf({ path: 'font-fixture.pdf', format: 'A4', waitForFonts: true });
} finally {
await browser.close();
}
})();
Run this fixture in CI whenever the Docker base, browser, Puppeteer version, or font inventory changes.
10. Performance, reliability, and cost
- Build fonts into the image once instead of downloading them for every job.
- Reuse a browser process when your workload allows it, but isolate jobs that require different profiles or credentials.
- Keep the installed font set focused: large collections increase image size and can complicate fallback selection.
- Use explicit navigation and font readiness timeouts so a failed font request cannot hang a worker indefinitely.
- Cache Docker layers so unchanged font packages do not reinstall on every build.
- Record the base image, browser version, Puppeteer version, and font package list with each release for reproducibility.
Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. One request returns a PNG, JPEG, WebP, or PDF, so you do not maintain Chrome dependencies or font packages in your own container. See the ScreenshotNeo API documentation for all options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
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)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
FAQ
Do I need to download fonts with Puppeteer code?
Usually no. Install them in the Docker image so Chrome can discover them at runtime. Downloading during each render makes builds slower and less reproducible.
Will installing one general font fix every language?
No. Coverage varies by script and font family. Test the exact characters your pages render.
Why does a screenshot look correct but the PDF does not?
The PDF may start before web fonts finish loading, or the page may be in the background. Await document.fonts.ready, call page.bringToFront(), and keep PDF font waiting enabled.
Should I use the official Puppeteer image?
It is the simplest route when you want Chrome, dependencies, and Puppeteer aligned. Use a custom image when you need tighter control over the operating system and font inventory.
Deployment checklist
- Choose and record the Linux base image.
- Confirm every font package name in that distribution’s current repositories.
- Install or copy fonts during the image build.
- Verify font files inside the built image.
- Test mixed scripts, fallback behavior, and emoji if required.
- Await web-font readiness before screenshots and PDFs.
- Provide writable Chrome profile and cache paths.
- Pin and record browser, Puppeteer, and font versions.


