ScreenshotNeo

BlogHow-to

How to Build and Upload a Custom Browser Image

Build a version-pinned Playwright browser image, push it to a container registry, and verify the tag. Includes Node.js and Python Dockerfiles, platform choices, and runtime guidance.

By the ScreenshotNeo team29 September 20269 min read

How to Build and Upload a Custom Browser Image

This guide assumes “custom browser image” means a Docker image containing Playwright and its browser dependencies, uploaded to a container registry. If you mean a different browser automation framework, its browser installation and operating-system dependency instructions need separate validation.

In short: choose a supported runtime base, pin the Playwright version, install the matching browser binaries and system dependencies, build an image tagged for your registry, then push and verify that tag. Keep the Playwright package version and browser image release aligned: a mismatch can leave Playwright unable to find its browser executable.

1. Decide what the image needs

A usable browser automation image combines three pieces: the application runtime, the framework package, and compatible browser binaries with their operating-system libraries. Playwright’s Docker documentation shows installing browsers and dependencies from Node.js and Python base images. Its published image includes browser binaries and system dependencies, but not the Playwright package; your project still needs to install that package.

A browser image needs the runtime, framework package, matching browser binaries, and operating-system dependencies.
A browser image needs the runtime, framework package, matching browser binaries, and operating-system dependencies.
Choice Use it when Watch for
Build from a Node.js or Python base You need control over the OS, package versions, and app layers. Install the same pinned Playwright version used by the project and install browsers with dependencies.
Start from the Playwright image You want the browser binaries and system libraries already present. Install the Playwright package yourself and pin the base image release to match it.
Single-platform build Your deployment target has a known CPU architecture. Build for that target architecture.
Multi-platform build The image must run on multiple CPU architectures. Specify platforms explicitly and push the result to a registry.

Use a supported base distribution. Playwright documents Ubuntu 22.04 Jammy, Ubuntu 24.04 Noble, and Ubuntu 26.04 Resolute variants for its image. Its Firefox and WebKit builds target glibc; Alpine’s musl environment is not supported for those builds. See the Playwright Docker guide for current image and runtime details.

2. Write a version-pinned Dockerfile

Choose a real Playwright release that your application uses, then pin it in both the dependency manifest and Docker build. The examples below use a build argument so the version is visible and can be set explicitly. Replace 1.x.y with the same concrete release in your project and build command; do not ship a floating value.

Node.js Dockerfile

FROM node:20-bookworm
WORKDIR /app
COPY package*.json ./
RUN npm ci
ARG PLAYWRIGHT_VERSION
RUN test -n "$PLAYWRIGHT_VERSION" \
    && npm install --no-save "playwright@$PLAYWRIGHT_VERSION" \
    && npx playwright install --with-deps
COPY . .
CMD ["node", "index.js"]

For a repeatable production build, put the selected Playwright version in your package manifest and lockfile rather than relying on an unrecorded install. A manifest entry might be "playwright": "1.x.y"; regenerate and commit the lockfile after choosing a real release. Then simplify the Dockerfile to RUN npx playwright install --with-deps. The argument form above is useful for illustrating the version requirement, but a committed lockfile is the stronger project-level record.

Python Dockerfile

FROM python:3.12-bookworm
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
ARG PLAYWRIGHT_VERSION
RUN test -n "$PLAYWRIGHT_VERSION" \
    && pip install --no-cache-dir "playwright==$PLAYWRIGHT_VERSION" \
    && playwright install --with-deps
COPY . .
CMD ["python", "main.py"]

As with Node.js, pin the dependency in a requirements or lock file used by the application. If the base image is the published Playwright image, install the matching package version separately because the image itself does not include that package. The browser executable revision bundled in the image must match the Playwright package expected by the application.

Build a local image

For either Dockerfile, pass the concrete version and choose a local tag. This example assumes a project lockfile pins Playwright; omit the argument if your Dockerfile no longer uses it.

docker build --build-arg PLAYWRIGHT_VERSION=1.x.y \
  --tag local/browser-worker:1.x.y .
docker run --rm local/browser-worker:1.x.y

Replace 1.x.y with a real version before running. The second command is a basic startup check, not a complete browser test; use your application’s normal smoke check to confirm it can launch the intended browser.

3. Tag and upload to a registry

An image name follows [HOST[:PORT]/]NAMESPACE/REPOSITORY[:TAG]. Docker Hub commonly omits the host; other registries need their host name. Pick a namespace you can publish to, a repository that describes the image, and a meaningful version tag. A release tag makes it possible to deploy the same image later and roll back deliberately.

Build for the target platform, push the chosen tag, then verify that exact tag in the registry.
Build for the target platform, push the chosen tag, then verify that exact tag in the registry.

Option A: build and push with Buildx

Authenticate with your registry first when required, then let Buildx push the build output directly. For Docker Hub, the target name can be yourname/browser-worker:1.x.y. For another registry, include its hostname, such as registry.example.com/team/browser-worker:1.x.y.

docker login

docker buildx build \
  --platform linux/amd64 \
  --build-arg PLAYWRIGHT_VERSION=1.x.y \
  --tag yourname/browser-worker:1.x.y \
  --push .

Use a concrete version and a namespace you control. To publish multiple architectures, provide the intended comma-separated platforms, for example --platform linux/amd64,linux/arm64, if the selected base image and browser dependencies support them. A registry is the usual destination for a multi-platform Buildx result. Docker documents the Buildx build options and exporters.

Option B: tag a local image and push it

If you built the image locally, apply the registry tag and push that tag. Docker Hub’s form is NAMESPACE/REPOSITORY:TAG; other registries use their host-prefixed image name.

docker login
docker tag local/browser-worker:1.x.y \
  yourname/browser-worker:1.x.y
docker push yourname/browser-worker:1.x.y

Docker manages registry credentials through docker login. Avoid putting passwords directly in shell commands or Dockerfiles. See Docker’s official instructions for pushing images to a repository and the image push command.

4. Verify the published tag

  1. Open the registry repository in the account or organization namespace you targeted.
  2. Find its Tags view and check that the exact version tag appears.
  3. Pull the tag from a clean environment or deployment runner to confirm access and availability.
  4. Start a container from that tag using the intended runtime settings, then run an application-level browser smoke check.

For Docker Hub, its documentation directs users to the repository’s Tags view to confirm a push. Seeing the tag in the registry confirms the upload; pulling and running it separately checks that consumers can retrieve and start that artifact.

5. Choose runtime flags and permissions

The right runtime setup depends on whether the image visits trusted test pages or arbitrary websites. Playwright says its published Docker image is intended for testing and development and is not recommended for visiting untrusted websites. It runs as root by default, which disables Chromium’s sandbox. Root may be acceptable for trusted end-to-end tests; for untrusted crawling or scraping, Playwright recommends a separate user and a seccomp profile that permits the required user namespace operations.

For Chromium, Playwright recommends --ipc=host because the default shared-memory setup can cause crashes. It also recommends --init to avoid PID 1 and zombie-process issues. A typical trusted test invocation is:

docker run --rm --init --ipc=host \
  yourname/browser-worker:1.x.y

Do not treat this example as a security profile for arbitrary web content. For untrusted targets, configure a non-root user and the seccomp profile appropriate to the workload and host. Playwright mentions --cap-add=SYS_ADMIN only as a local-development troubleshooting step for unusual Chromium launch errors; adding broad capabilities in production should not be the default fix. More detail is in the official Playwright runtime guidance.

6. Make builds reproducible and practical

  • Pin framework and image together. Use an explicit Playwright release and matching browser build. Update them as a unit, then rebuild and smoke-check.
  • Use a lockfile. Copy the lockfile before dependency installation and use the package manager’s deterministic install command, such as npm ci. For Python, pin Playwright and application dependencies in the requirements or lock file.
  • Keep the build context small. Add a .dockerignore for local caches, virtual environments, source-control metadata, and secrets. Do not copy credentials into layers.
  • Choose platform deliberately. A single-platform image is straightforward when deployment architecture is fixed. Multi-platform builds make one tag usable on several architectures, but must be built for the intended platforms and pushed to a registry.
  • Use release tags. Tags like 1.x.y communicate the framework version. A floating tag can be useful as a moving pointer, but should not be the only reference for a production deployment that needs repeatable rollbacks.

Browser binaries and operating-system dependencies make browser images larger than a minimal application image. Build time and transfer time depend on the chosen base, downloaded packages, cache state, and target architectures; the cited documentation provides no universal size or timing figure. Reuse build cache where appropriate and avoid reinstalling browsers in every application layer. On failure, compare the package version and browser revision before changing unrelated settings.

7. Troubleshooting

Symptom Likely cause What to check or change
Playwright cannot find an executable The framework package and browser binaries are different releases, or browser installation did not run. Pin the package and image/build install to the same Playwright version; run the browser installation step in the image build.
Browser exits or crashes under load Chromium has insufficient shared memory in the container. Try the documented --ipc=host runtime setting and inspect container memory limits.
Zombie processes accumulate The container’s main process is not reaping child processes. Use the documented --init option.
Chromium launch reports an unusual permission error Sandbox or container permissions may not match the workload. Review the Playwright Docker guidance and trust model. --cap-add=SYS_ADMIN is described as a local-development troubleshooting step, not a general production setting.
Firefox or WebKit dependencies fail on Alpine The documented Playwright browser builds target glibc, while Alpine uses musl. Use a supported glibc-based image such as the documented Debian/Ubuntu choices.
Push is denied Wrong namespace, missing login, insufficient registry permission, or an incorrectly tagged image. Check the full image reference, run docker login for the target registry, and confirm your account can publish to that repository.
Tag is absent after push The command may have pushed a different name or tag than expected. Inspect the exact tagged reference in the push command and check the repository’s Tags view.
Image pulls but the app fails to start The image may lack application files, runtime configuration, or required environment variables. Run the exact tag locally with deployment-equivalent configuration and inspect container logs.

8. If the goal is screenshots, skip the browser setup

If you need screenshots from your application rather than a browser image to manage, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. The API accepts familiar screenshot parameter names, which can make migration from another screenshot API easier. See the ScreenshotNeo API documentation for request options and configuration.

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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await (await import('node:fs/promises')).writeFile('shot.webp', bytes);

ScreenshotNeo accepts cookie and consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each of those steps can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses identify page verdict and billing status in headers. Its MCP server offers take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots. Every feature is available on every plan.

Start with 1,000 screenshots a month free, with no card: create a ScreenshotNeo account.

9. Frequently asked questions

Do I need to publish an image to run it?

No. A local build can run on the same machine. Push it when another machine, deployment platform, or team needs to pull that exact image.

Does the published Playwright image include the Playwright library?

No. It includes browser binaries and system dependencies; install the matching Playwright package in your project image.

Can I use one image for every browser?

The documented installation flow can install browser builds and dependencies, but the exact set should match the browsers your application uses. Keep the framework and browser revisions aligned.

Should I use the same image for untrusted websites?

Follow Playwright’s separate-user and seccomp guidance for untrusted crawling or scraping. The published image is described as intended for testing and development, not visiting untrusted sites.

Official references