Running tests in Docker
Docker solves one problem for automated tests: "it works on my machine". Browser version, system libraries, fonts, locale, timezone — all pinned in the image, so the local machine, every team member and CI run in the same environment.
The official image: mcr.microsoft.com/playwright:v1.49.0-noble — it ships with the browsers (Chromium, Firefox, WebKit) and all system dependencies. That removes the npx playwright install --with-deps step and the 2-3 minutes it costs.
The most important rule: the image tag must match the @playwright/test version in package.json. A mismatch gives you either an "Executable doesn't exist" error or, worse, tests behaving unexpectedly. This is the number one trap in the topic and must be remembered on every dependency bump.
When I reach for Docker:
- In CI — for environment stability and a fast start
- To generate visual-test baselines — identical font rendering to CI matters
- While investigating "passes locally, fails in CI" — reproducing CI conditions locally
- To bring up the whole stack (app + DB + tests) — with docker compose
A full environment with docker compose — tests, the application and the database come up together. This is especially valuable because it gives every PR a clean, isolated environment: the shared-stage problem of "someone changed the data" disappears.
There are several classic traps here, and these are exactly what interviews ask about:
- `localhost` doesn't work. A container has its own network namespace;
http://localhost:3000points at the container itself. The correct address is the service name:http://app:3000. - `/dev/shm` exhaustion. Docker gives 64MB of shared memory by default; Chromium burns through it and crashes with cryptic errors like
Target closedorBrowser crashed. Fix:--shm-size=1gb(or--ipc=host). This is a very common but rarely recognised cause of CI flakiness. - The service isn't ready. A container starting ≠ the app responding.
depends_onalone isn't enough — you need ahealthcheck+condition: service_healthy, otherwise tests start while the app is still booting. - File ownership (on Linux). If the container runs as root, the artifacts it creates (
playwright-report/,test-results/) end up root-owned and undeletable on the host. Fix:-u $(id -u):$(id -g). - Volume mounts and `node_modules`. Mounting the host's
node_modulesinto the container breaks on platform-specific binaries — either runnpm ciinside the image or excludenode_moduleswith an anonymous volume. - Layer caching. The order
COPY package*.json→RUN npm ci→COPY . .matters; otherwise every code change reinstalls all dependencies.
| Symptom | Cause | Fix |
|---|---|---|
| "Executable doesn't exist" | The image tag doesn't match the @playwright/test version | Align the tag with the package.json version |
| Chromium crashes randomly, "Target closed" | /dev/shm fills at the 64MB default | --shm-size=1gb or --ipc=host |
| ECONNREFUSED localhost:3000 | The container is looking at its own namespace | Use the service name in BASE_URL: http://app:3000 |
| The first tests fail, later ones pass | The app hasn't finished booting | healthcheck + condition: service_healthy |
| Artifacts can't be deleted on the host | The container ran as root | -u $(id -u):$(id -g) |
| npm ci reruns on every build | COPY . . comes BEFORE the dependency install | Copy package*.json first, then npm ci, then the rest |
A strong concrete detail for interviews: `--shm-size=1gb`. The sentence "Chromium kept crashing randomly in Docker; the cause was the default 64MB /dev/shm" is one of the most convincing signals of real experience — only someone who hit it knows it. Likewise the service name instead of localhost, and matching the image version, separate "read about it" from "built it".
📚 Sources and documentation
- Dockerofficialplaywright.dev