Your CI job downloads Chromium fine, then dies at launch with an error about a missing shared library. That is not a misconfiguration on your side. The browser binary and the system packages it needs are two separate installs, which is why Playwright ships a separate install-deps command and a --with-deps flag at all. Those commands shell out to the OS package manager, so they need root at image build time, and the browsers themselves take a few hundred megabytes of disk space.
Four ways out, in rough order of how much rendering infrastructure you keep.
Install the dependencies properly
The unclever option is often the right one. Playwright publishes ready-made images to the Microsoft Artifact Registry under mcr.microsoft.com/playwright, tagged by Playwright version and Ubuntu base. Those images include the browsers and the browser system dependencies, but not the Playwright package itself, so you still install that on top. Or build your own, which is what Playwright's docs suggest:
FROM node:20-bookworm
RUN npx -y playwright@1.57.0 install --with-deps chromiumThree things bite here. Pin the image tag to the Playwright version in your project, or Playwright cannot locate the browser executables. Run the container with --ipc=host, because without it Chromium can run out of memory and crash mid-render. And Alpine will not work: Playwright's Firefox and WebKit builds target glibc, so musl-based distributions are unsupported. If you only ever render headlessly, --only-shell skips the full Chromium download and installs just the headless shell.
Move the browser out of the job
Run the renderer as a service instead of a library. Gotenberg is the usual pick: one MIT-licensed container bundling Chromium and LibreOffice behind an HTTP API, published for amd64, arm64, armhf, i386 and ppc64le.
docker run --rm -p "3000:3000" gotenberg/gotenberg:8
curl \
--request POST http://localhost:3000/forms/chromium/convert/url \
--form url=https://my.url \
-o my.pdfYour job now needs an HTTP client and nothing else, and most CI systems can start this alongside the job as a service container. You are still running a browser, but the dependency problem lives inside someone else's image instead of your Dockerfile.
Render without a browser at all
If the output is a document (an invoice, a statement, a report) rather than a screenshot of your running app, you may not need a browser engine. WeasyPrint renders HTML and CSS to PDF from pure Python, with no Chromium anywhere in the dependency tree:
from weasyprint import HTML
html = "<h1>Invoice 1042</h1><p>Total: $4,200.00</p>"
HTML(string=html).write_pdf("out.pdf")It does lay out flexbox and grid, though WeasyPrint's own documentation describes its flexbox support as working "for simple use cases but is not deeply tested" and its grid support as working "for simple cases, but has some limitations". Check your specific layout rather than assuming parity with Chrome. The harder constraint is that WeasyPrint does not run JavaScript: a script tag that rewrites the DOM is ignored, and you get the server-rendered markup. If your page builds its content client-side, render it server-side first and hand WeasyPrint the finished HTML.
Do not run a renderer
Post your HTML to a hosted endpoint and get PDF bytes back. The job needs an HTTP client and a credential. The trade-offs are real: per-document cost, a network dependency inside your pipeline, and your document content leaving your infrastructure, which is worth settling with whoever owns compliance before you commit. In exchange, the rendering environment stops being something you maintain, and font drift stops being something you debug. Anvil's PDF generation API is one option in this category, taking HTML and CSS or Markdown and returning a PDF.
Picking one
Let the document decide. If you are snapshotting a live dashboard whose charts are drawn by JavaScript after load, you need a real browser, so the first two options are your only honest choices. If you are generating a templated invoice from data you already have, the last two are usually less to maintain, and nothing in the pipeline has to know which shared libraries Chromium wants this month.
Back to All Questions