HTML to Image Conversion with Playwright on Alpine Linux
Playwright does not support running its browsers directly on Alpine Linux. Use a supported browser container or connect Alpine to a remote Playwright server.
Playwright does not officially support running its browser binaries directly on Alpine Linux: Alpine uses musl, while Playwright’s browser builds require glibc. The practical choices are to run the browser in a supported container, or keep your application on Alpine and connect it to a remote Playwright server running in a supported environment. Installing the Playwright npm package in an Alpine image alone does not solve the browser compatibility problem.
Once a compatible browser is available, Playwright can render HTML and save a screenshot as PNG, JPEG, or WebP. Use fullPage for the whole scrollable document, or leave it off for the current viewport.
Playwright states that Alpine Linux and other distributions based on musl are not supported. This guide shows both deployment patterns, complete Node.js examples, output controls, operational guidance, and common fixes.
1. Choose where the browser runs
| Approach | Use it when | What to keep aligned |
|---|---|---|
| Supported browser container | Your rendering service can use a supported Linux base image. | The Playwright package version, browser image version, and installed browser binaries. |
| Remote Playwright server | Your application must stay on Alpine, but browser execution can run elsewhere. | The client and server Playwright versions, plus network access to the server endpoint. |
For the simplest deployment, run the rendering service and browser together in a supported Playwright container. If Alpine is a hard requirement for the application image, separate browser execution into a supported container and connect to it over your private deployment network. See the official Playwright Docker guidance and browser installation documentation for supported images and dependency details.
2. Run Playwright in a supported container
Use an official Playwright image as the runtime for the service that launches the browser. The image and the project’s Playwright package must match. This Dockerfile pins both to the same version; update the package and image tag together when upgrading.
FROM mcr.microsoft.com/playwright:v1.55.0-noble
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "capture.mjs"]
Example package.json:
{
"name": "html-to-image",
"private": true,
"type": "module",
"scripts": { "capture": "node capture.mjs" },
"dependencies": { "playwright": "1.55.0" }
}
Commit the lockfile so npm ci installs the pinned dependency. The version shown is an example of a matched tag and package version; select a currently available official image and use its exact version tag with the same Playwright package version.
Create capture.mjs:
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage({
viewport: { width: 1440, height: 1000 },
deviceScaleFactor: 1
});
// For a file, use: await page.goto('file:///app/input.html');
await page.setContent(`
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font: 16px system-ui; margin: 32px; color: #172033; }
.card { padding: 24px; border-radius: 16px; background: #eef3ff; }
</style>
</head>
<body>
<article class="card"><h1>Rendered HTML</h1><p>Captured with Playwright.</p></article>
</body>
</html>
`, { waitUntil: 'load' });
await page.screenshot({
path: 'output.png',
type: 'png',
fullPage: true
});
} finally {
await browser.close();
}
Build and run from the directory containing the Dockerfile, package files, and script:
docker build -t html-to-image .
docker run --rm -v "$PWD:/app" html-to-image
The bind mount makes output.png available in the current host directory. In a production service, write to an explicitly managed output directory or object storage instead of relying on a container’s writable layer.
3. Keep the application on Alpine and use a remote browser
Run Playwright Server in a supported container and make its WebSocket endpoint reachable only to the application over the deployment network. Do not expose an unauthenticated browser-control endpoint to the public internet. The official Docker guide documents the remote-server pattern for unsupported distributions.
In the supported browser container, start the server using the same pinned Playwright version as the client. For example, with the package installed in that container:
npx playwright run-server --host 0.0.0.0 --port 3000
In the Alpine application, install the matching package and connect with the documented browser connection API:
import { chromium } from 'playwright';
const endpoint = process.env.PLAYWRIGHT_WS_ENDPOINT;
if (!endpoint) throw new Error('Set PLAYWRIGHT_WS_ENDPOINT');
const browser = await chromium.connect(endpoint);
try {
const page = await browser.newPage({
viewport: { width: 1440, height: 1000 },
deviceScaleFactor: 1
});
await page.setContent('<main><h1>Rendered remotely</h1></main>', {
waitUntil: 'load'
});
await page.screenshot({ path: '/tmp/output.png', fullPage: true });
} finally {
await browser.close();
}
Set PLAYWRIGHT_WS_ENDPOINT to the WebSocket endpoint reachable from the Alpine application. Configure the server’s listen address and network exposure for your container platform. The exact endpoint address depends on that platform’s service discovery. Keep the browser server and client package versions identical; do not assume that any reachable Playwright server is compatible.
4. Choose HTML input and wait for rendering
HTML strings
page.setContent(html) is suitable for generated markup. Include CSS and assets in the HTML or serve them from addresses the browser can reach. If external resources matter, wait for the relevant load condition or a specific element before capture. A navigation load event alone does not guarantee that client-side rendering, web fonts, or lazy content is ready.
Local HTML files
Navigate to a file URL such as file:///app/input.html. Ensure the file exists inside the browser container at that path; a host file is not automatically visible inside a container. For remote browser execution, the path is on the remote browser host, not the Alpine client. An alternative is to send the HTML to the browser with setContent.
Web pages
For a URL, use await page.goto(url, { waitUntil: 'networkidle' }) when network activity settles reliably. Some sites keep connections open or poll continuously, so a fixed selector or a bounded delay can be more reliable for those pages. Always give navigation and the overall capture job finite timeouts.
5. Configure the screenshot output
The core method is page.screenshot(options). Check the Page API for the complete option reference.
| Need | Option or choice | Effect |
|---|---|---|
| Current viewport | Default; set viewport when creating the page |
Captures the visible page area at the configured viewport dimensions. |
| Entire scrollable page | fullPage: true |
Captures the full page rather than only the viewport. |
| Format | type: 'png', 'jpeg', or 'webp' |
Choose an image format supported by the Page API. PNG is the default when omitted. |
| JPEG quality | quality: 0–100 |
Controls JPEG compression; the quality option applies to JPEG. |
| CSS-sized output | scale: 'css' |
Output dimensions follow CSS pixels. |
| Device-pixel output | scale: 'device' |
Output dimensions account for the device scale factor. |
| Transparent background | omitBackground: true |
Removes the default page background where transparency is supported. JPEG cannot represent transparency. |
| Capture one element | locator(selector).screenshot(...) |
Captures a selected element instead of the page. |
For a transparent PNG, for example:
await page.screenshot({
path: 'transparent.png',
type: 'png',
omitBackground: true,
scale: 'css'
});
For an element-only image:
await page.locator('.card').screenshot({ path: 'card.png' });
For very tall documents, full-page capture can produce large images and use substantial memory. Consider capturing a specific element or splitting the content into sections if the output does not need every page region in one bitmap.
6. Stabilize rendering and manage reliability
- Pin the operating system image, Playwright package, browser binaries, and headless configuration. Browser versions are tied to Playwright releases; reinstall browser binaries as part of an upgrade rather than reusing stale binaries.
- Use the same environment to create and compare visual baselines. Host OS, browser version, settings, hardware, and headless mode can change rendered output. See Playwright’s visual comparisons guidance.
- Wait for the content that matters. Prefer a selector for application-specific readiness; wait for fonts or images when they affect the result. Do not use an unbounded wait for network idle on pages that never become idle.
- Set finite navigation and job timeouts. Close pages and browsers in
finallyblocks so failures do not leave browser processes running. - For remote execution, monitor endpoint reachability and capacity in your own deployment. Keep it on a private network and handle reconnects as a job retry, with an upper bound so transient failures do not create unlimited work.
- Use isolated browser contexts for unrelated pages or users when session state must not be shared. Avoid reusing cookies or other authenticated state accidentally.
Playwright’s documentation does not establish a universal capture throughput or image-fidelity guarantee for this deployment. Measure your own pages and concurrency in the environment you plan to operate.
7. Performance and cost considerations
Browser startup, page navigation, application rendering, image encoding, and output transfer all contribute to latency. Reusing a browser process for a batch of jobs can avoid repeated startup overhead, but isolate page or context state and close resources reliably. A remote browser also adds network latency and a service to operate. Measure representative pages, including slow external assets and long pages, before choosing concurrency.
Playwright itself is software rather than a per-screenshot API in this setup. Budget for the compute, memory, storage, and operations required to run browser containers, plus any remote-browser infrastructure. Full-page and device-scale captures can increase output dimensions and memory use; choose dimensions and format according to the consumer’s needs. No fixed cost or speed figure applies across deployments.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Browser executable missing or launch fails on Alpine | Playwright browser builds are not supported on Alpine’s musl environment. | Run the browser in a supported container and connect remotely, or move the rendering service to a supported base image. |
| “Executable doesn’t exist” after changing package versions | The installed browser binaries do not match the Playwright package or image. | Pin compatible versions and install the browsers for that Playwright release; rebuild the image after upgrades. |
| Remote connection fails | Wrong WebSocket endpoint, blocked network route, server not listening on a reachable interface, or client/server version mismatch. | Check service DNS and port access from the Alpine container, confirm the server is listening on the intended interface, and align versions. |
| Screenshot is blank or content is missing | Capture happened before client rendering or required assets completed; external URLs may also be unreachable from the browser container. | Wait for a page-specific selector or asset readiness, verify resource access from the browser host, and capture after it appears. |
| Fonts or layout differ between runs | Different OS, browser, fonts, hardware, headless configuration, or page state. | Keep the rendering environment fixed and generate comparison baselines in that same environment. |
| Transparent output has a solid background | The screenshot omitted omitBackground: true, or the selected format is JPEG. |
Set omitBackground: true and use PNG or another transparency-capable format; JPEG does not support transparency. |
| Full-page image is unexpectedly huge | The document is very tall, the device scale factor is high, or both. | Use CSS scale, reduce viewport or content dimensions, capture a relevant element, or split the page into sections. |
| Navigation hangs | The site never reaches the chosen wait condition, such as network idle. | Use a finite timeout and wait for the specific content needed for the screenshot. |
9. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request can return a PNG, JPEG, WebP, or PDF. For a URL capture, the request looks like this:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API docs for request options. The same basic request in Python and Node.js:
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 import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
- Cookie banners, popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Responses say which page verdict occurred and whether it was billed.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdftools for AI agents and MCP clients. - The free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots. Every feature is available on every plan.
Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.
10. FAQ
Can I use a different browser engine?
This guide covers Playwright’s browser execution and screenshot API. The Alpine limitation described here concerns Playwright’s browser builds; choose a supported runtime for the browser engine you intend to run.
Does fullPage: true change the viewport?
It changes the capture area to include the full scrollable page. Set the viewport separately to control the page’s CSS layout width and height.
Will this produce identical pixels on every machine?
No. Rendering can vary with the operating system, browser version, settings, hardware, and headless configuration. Keep the environment consistent for repeatable comparisons.
Can my Alpine application read a file from the remote browser container?
A local path is interpreted by the browser host. Make the file available to that host or send its HTML content to the page; the Alpine filesystem is not automatically shared with the remote container.


