How to fix Failed to launch chromium in a Docker agent image
The build goes green. The image pushes to the registry. Then the container starts, the agent reaches its first chromium.launch(), and it dies with Executable doesn't exist at .... Nothing in a normal Docker build launches a browser, so nothing in CI ever tripped. You find out at container start, on the server, usually when a scheduled run was meant to happen.
This is the question behind microsoft/playwright issue 4033, and the short answer is that pip install playwright or npm install playwright installs the library, not the browser. The browser is a separate download. It lands in a per-user cache directory, and that directory is easy to lose between the build and the running container.
This is for operators running Playwright browser agents in Docker, Python or Node. By the end you’ll have a Dockerfile that puts chromium in a fixed path, runs as a non-root user, and fails the build instead of the deploy if the browser can’t start. Examples are Python; for Node use npx playwright install. I’ve only run this on Debian based images on x86-64, so treat arm64, firefox and webkit as untested by me.
what you need
- Docker Engine or Docker Desktop with BuildKit, which current releases enable by default. Engine on Linux is free. Docker Desktop has paid tiers for larger companies, so check Docker’s pricing page for the terms as of September 2026.
- a Playwright project with the version pinned exactly in
requirements.txtorpackage-lock.json. - the Dockerfile of the failing agent image, and a shell where you can build and run it.
- some spare disk. The browser and its apt packages make the image noticeably bigger than bare python-slim, so compare
docker imagesbefore and after. - no paid accounts. Registry storage and CI minutes are the only real costs.
step by step
1. Reproduce the error and read the path
Run the image the way your deploy does.
docker run --rm my-agent:latest
Expected output, roughly (your revision number will differ):
playwright._impl._errors.Error: BrowserType.launch: Executable doesn't exist at /home/agent/.cache/ms-playwright/chromium_headless_shell-1148/chrome-linux/headless_shell
Looks like Playwright was just installed or updated.
Please run the following command to download new browsers:
playwright install
The path tells you two things: which home directory Playwright searched, and which browser revision the installed library expects. Keep both for step 2.
If it breaks: a message like “Host system is missing dependencies to run browsers” means the browser exists but the OS libraries don’t. Step 4 covers that.
2. Work out which cause you have
docker run --rm --entrypoint sh my-agent:latest -c \
'id -un; echo "HOME=$HOME"; ls -d /root/.cache/ms-playwright/* "$HOME"/.cache/ms-playwright/* /ms-playwright/* 2>&1'
docker run --rm --entrypoint pip my-agent:latest show playwright
Expected output: a user name, a home path, then either directory listings or “No such file” lines. Read them like this:
- nothing found anywhere:
playwright installnever ran in the Dockerfile, or it ran in a build stage that never reached the final image. - browsers under
/root/.cachewhile the container runs as another user: the classic case. The installer ran as root, the agent runs asagent, and Playwright looks in the agent’s own home. - browsers found, but the revision differs from the one in step 1: the library and the browser came from different versions.
If it breaks: if --entrypoint sh fails, the image has no shell (distroless or scratch). Playwright is a poor fit there, so use a Debian or Ubuntu base for anything that launches chromium.
3. Pin the Playwright version in one place
The browser revision is tied to the library version, and playwright install fetches whatever the installed library expects. So the two only drift apart when they come from different places: a floating playwright>=1.40 in requirements plus a cached layer from last month, or a pip version that doesn’t match an official image tag. Pin it exactly:
playwright==1.xx.0
Use the version you have tested, then check with pip freeze | grep -i playwright. Expected output is one line with that exact version.
If it breaks: pip reports a resolver conflict with a wrapper such as pytest-playwright. Loosen the wrapper, not the pin.
4. Install chromium into a fixed path at build time
This is the actual fix.
FROM python:3.12-slim-bookworm
# fixed path, so it does not depend on whose HOME the process has
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt \
&& playwright install --with-deps chromium \
&& chmod -R a+rX /ms-playwright \
&& rm -rf /var/lib/apt/lists/*
RUN useradd --create-home --shell /bin/bash agent
COPY --chown=agent:agent . .
USER agent
CMD ["python", "agent.py"]
What each part does:
PLAYWRIGHT_BROWSERS_PATHis documented on Playwright’s browsers page. Moving the cache out of the home directory removes the root versusagentsplit, and becauseENVvalues persist into the running container (Dockerfile reference), the runtime process sees the same path.--with-depsinstalls the OS libraries with apt and needs root, so it runs beforeUSER. Namingchromiumskips firefox and webkit. In recent releases headless launches use a separate headless shell build andplaywright install chromiumfetches it too, which is why your error path may sayheadless_shell.- pip and playwright share one
RUNso their cache layers expire together.
You can start from an official image instead. Playwright’s Docker docs publish tags like v1.xx.0-noble, and the tag must equal your library version exactly. I still prefer my own base, because I can see what is in it and choose the Python version, though you could argue the official image’s system dependencies are better maintained by the people who wrote the library. If you’re eyeing newer Ubuntu bases, I covered whether Playwright runs on Ubuntu 26.04 LTS yet. Which chromium flavour you run is a separate decision, compared in Chrome for Testing vs open-source Chromium for a Playwright agent fleet.
Expected output: apt installs, then chromium download lines, then a finished build.
If it breaks: a warning that your OS is not officially supported means the base image isn’t one Playwright targets. Switch to a Debian or Ubuntu tag. I stay off alpine for this reason.
5. Confirm the non-root user can see the browser
docker build -t my-agent:test .
docker run --rm --entrypoint sh my-agent:test -c 'id -un; ls /ms-playwright'
Expected output: agent, then a listing with chromium directories.
If it breaks: “No such file” usually means ENV sits after the install RUN, so the download went to the default cache. “Permission denied” means the chmod line is missing.
6. Move the failure to build time with a smoke launch
Add smoke_launch.py to the project:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
print("chromium", browser.version)
browser.close()
Then add this line to the Dockerfile after USER agent and before CMD, so it runs as the same user and HOME as production:
RUN python smoke_launch.py
Expected output in the build log: chromium followed by a version string. A missing browser is now a red build in CI, not a dead container at 3am.
If it breaks: error while loading shared libraries or “Host system is missing dependencies” means the apt step didn’t complete. Check that --with-deps is in the install command.
7. Run it with the flags chromium wants
docker run --rm --init --ipc=host my-agent:test
Playwright’s Docker docs recommend --ipc=host for chromium, because without it chromium can run out of memory and crash, and --init to avoid zombie processes. Docker’s default /dev/shm is 64 MB, which is small for a browser.
Expected output: the agent finishes its first browser task.
If it breaks: “Page crashed” or “Target closed” partway through is a shared memory problem, not a missing binary. You got past the launch, which is the point. Try --shm-size with a larger value.
8. Check what your orchestrator puts over the image
Compose volumes, Kubernetes persistent volumes and deploy scripts can hide or replace paths. Check inside the running container:
docker compose exec agent ls /ms-playwright
kubectl exec deploy/agent -- ls /ms-playwright
Using /ms-playwright instead of the home directory helps here, since home mounts no longer hide the browser. A volume mounted at /ms-playwright, or a deploy that pulls a stale latest tag, still can.
Expected output: the chromium directories. If it breaks: deploy by image digest or a unique tag rather than latest, and confirm the running pod’s image ID matches the one you built.
common pitfalls
- Running
playwright installin the entrypoint. It makes the error go away and I still say don’t. It downloads on every container start, needs outbound network and a writable disk, races when replicas boot together, and turns a build problem into a slow-start problem. - Installing as root, running as another user, and leaving the cache in its default location. Step 2 shows how to spot it.
- Multi-stage builds that copy
/ms-playwrightinto the final stage but not the apt libraries. The browser is there and launch still fails on shared libraries. Run the smoke launch in the final stage. - Leaving
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1set from a base image or CI config, which can make installs skip the download. Runenv | grep PLAYWRIGHTduring the build. - Trusting a green build. Green only means the image assembled. The browser agent production checklist has the runtime checks I’d add next.
scaling this
- 10x: ten containers, one image, one Dockerfile. Nothing above changes, and the smoke launch in CI is the only guard you need.
- 100x: pull time and image size start to show. Keep the browser layer early and stable so only the agent code layer changes per deploy, and pre-pull images onto nodes. Kubernetes usually won’t give you
--ipc=host, so mount anemptyDirwithmedium: Memoryat/dev/shm. Upgrade Playwright by rebuilding once and rolling every replica together, and log the chromium version at startup so traces and replays show which build ran. - 1000x: build once, promote the same digest through environments, and put a pull-through cache or regional mirror in front of the registry so nodes don’t all hit one place. At this size I’d consider pulling chromium out of the agent image into a small pool of browser containers the agents connect to over Playwright’s remote connection. I haven’t run that setup at this size myself, so weigh the extra network hop and lifecycle management before copying it. Also expect launch failures to stop being your main problem. Site-side blocking takes over, and why AI browser agents get blocked and how IP reputation is earned and lost explain that side, for agents working on your own properties or with consent and within each site’s terms.
where to go next
- How to fix ERR_TUNNEL_CONNECTION_FAILED when a cloud browser agent connects through a proxy: the next error many agents hit once the browser finally launches.
- How to fix Chrome 136 refusing to be driven over CDP with your default profile: for when you attach to a real Chrome instead of launching Playwright’s own build.
- How to add human approval checkpoints to an AI agent: once the container is stable, decide which actions need a person to say yes.
Every tutorial on the site is in the blog index.
Written by Xavier Fok
disclosure: this article may contain affiliate links. if you buy through them we may earn a commission at no extra cost to you. verdicts are independent of payouts. last reviewed by Xavier Fok on 2026-09-20.