playwright-e2e-in-docker-sandbox
Skill0 starsChanged 5 days agoMITView on GitHub
Run a Playwright e2e suite inside the network-isolated claude-code-docker-sandbox container with zero runtime egress and no credentials, by baking Chromium into the image at build time (the build runs before the firewall). Use when e2e must run in the container (Playwright CDN blocked, `browser not found`), or when a credential-free `wrangler dev` accepts the connection but every request hangs, caused by a rate-limit binding proxying to a remote Cloudflare resource (the wrangler 3.x `unsafe` form) or a `localhost` bind stalling on dual-stack resolution (bind `--ip 127.0.0.1`). Covers the Dockerfile bake block, `--no-sandbox` gated on DEVCONTAINER, exact version pinning, and neutralising the `.dev.vars` copy the vite plugin writes into dist.
- Reads credentials
In this repository23 other files it gives its agents.
- skills/vercel-react-best-practices/AGENTS.mdAGENTS.md
- agent-status-hubSkill
- claude-code-docker-sandboxSkill
- cloudflare-api-token-permissionsSkill
- cloudflare-cron-to-discordSkill
- cloudflare-d1-drizzle-migrationSkill
- cloudflare-d1-keyless-host-backupSkill
- cloudflare-d1-weekly-backup-via-prSkill
- cloudflare-mcp-claude-toolingSkill
- cloudflare-r2-private-image-uploadSkill
- cloudflare-workers-bot-scan-defenseSkill
- cloudflare-workers-builds-keyless-deploySkill
- cloudflare-workers-deploy-skeletonSkill
- cloudflare-workers-e2e-playwrightSkill
- cloudflare-workers-host-synthetic-checkSkill
- cloudflare-workers-passkey-authSkill
- cloudflare-workers-pat-bearer-authSkill
- cloudflare-workers-space-membership-inviteSkill
- cloudflare-workflows-for-long-tasksSkill
- sandboxed-agent-github-token-via-1passwordSkill
- sandboxed-agent-git-relaySkill
- snr-writingSkill
- vercel-react-best-practicesSkill
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 1 is distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 13
If you develop in a network-isolated Docker sandbox (egress firewall, no host browser, no Cloudflare login — the claude-code-docker-sandbox …
- Distinctive: line 39
Do not use for:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Playwright e2e inside the Docker sandbox (credential-free, zero runtime egress)
If you develop in a network-isolated Docker sandbox (egress firewall, no host browser, no
Cloudflare login — the claude-code-docker-sandbox
setup), a Playwright suite can still run in-container with zero runtime egress. The
enabling idea is that sandbox skill's core rule: the firewall is the runtime
entrypoint, so anything whose network need is only at build time can be baked into the
image and never touches the runtime allowlist. A browser is exactly that — bake it.
This skill assumes the e2e suite itself is already wired per
cloudflare-workers-e2e-playwright
(build-artifact serving, --persist-to, auth seams, 3-spec scope). What lives here is
only the sandbox-specific layer: the bake, and the two traps that only bite when
wrangler dev runs credential-free.
When to use this skill
- e2e must run inside the sandbox container: there's no host browser, and the egress
firewall blocks the Playwright CDN, so a runtime
playwright installcan't work pnpm e2ein-container fails withbrowser not found- A credential-free
wrangler dev(sandbox, or a logged-out host) logsReadyand accepts TCP, but every request hangs with no response — Traps 1-2 below - The request after an early-rejected POST (401/403 answered before the body was read)
is a 500
Network connection lostandwrangler devexits — not a sandbox trap: see Trap 3 ofcloudflare-workers-e2e-playwright - You're deciding whether in-container e2e needs new egress-allowlist entries (it needs zero — that's the point)
Do not use for:
- Wiring the e2e suite itself (the CSP/HMR trap,
--persist-to, WebAuthn/OAuth seams, test scope) — that'scloudflare-workers-e2e-playwright - Non-sandboxed environments — a plain
playwright install chromiumon the host is fine
Deliverables (completion criteria)
- Browser baked at image-build time via the
INSTALL_PLAYWRIGHTbuild arg — zero runtime egress, noinit-firewall.shchange ARG PLAYWRIGHT_VERSIONexactly equals@playwright/testinpackage.json, and both are bumped together (rebuild after)- Chromium's sandbox left at Playwright's default (off) inside the container; to
sandbox host runs,
chromiumSandbox: !process.env.DEVCONTAINER - The built e2e config strips the rate-limit binding (
unsafeandratelimits— the v4 key no longer hangs, see Trap 1, but stripping keeps e2e independent of the limiter) - The
.dev.varscopy that@cloudflare/vite-plugin1.x writes intodist/<worker>/is neutralised — deleted byprepare-config, or every key overridden with--var wrangler devbinds--ip 127.0.0.1(notlocalhost), andORIGIN+ PlaywrightbaseURLuse the same literal host
Bake the browser at image-build time
docker compose build runs before init-firewall.sh (the runtime entrypoint), so the
build has open network — fetch Chromium and its OS libs there. At runtime the CDN is
blocked, but nothing needs it: e2e drives a local wrangler dev on 127.0.0.1, and
with the OAuth round-trip replaced by the seeded-session seam there is no external
egress at all during the run.
Opt in via two knobs (the full Dockerfile block is the canonical copy in references/in-container-playwright-bake.md — don't duplicate it into your own notes; drift between copies is exactly how the version-pin trap happens):
# docker-compose.yml — then: docker compose down && docker compose build && docker compose up -d
build:
args:
INSTALL_PLAYWRIGHT: "true" # adds ~0.4 GB to the imageKey rules the block implements (the "five details" in the reference):
PLAYWRIGHT_BROWSERS_PATH=/ms-playwright + chmod a+rX (root installs, node reads),
apt-get update before --with-deps, PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 at runtime,
and the exact version pin in two places: the runtime CDN is blocked, so an
ARG PLAYWRIGHT_VERSION ≠ @playwright/test drift cannot self-heal — it surfaces as
browser not found until you bump both and rebuild.
The pin is also a compatibility choice, not just a drift guard: playwright@1.59.1's
install chromium hangs forever right after the download completes on a node:24
base (no error, no #N DONE; node:22 is fine, 1.62.1 on node:24 is fine —
verified 2026-08-22 in matatabetai). If the bake step sits at 100% with no further
output, test the pin × base combination standalone before blaming the CDN:
docker run --rm -e DEBUG=pw:install node:24 npx -y playwright@<ver> install chromium.
The variant both production users actually run (matatabetai 2026-08-22, kokemusu
2026-09-02, node:24 / playwright 1.62.1, ~3 min build): the Chromium apt libraries as an
explicit list in the root apt layer (plus fonts-noto-cjk — a Japanese UI renders tofu in
failure screenshots without it), then npx -y playwright@${PLAYWRIGHT_VERSION} install
chromium as node after the Claude install, so the browser lands in
/home/node/.cache/ms-playwright (an image layer — never mount a volume there) and no
PLAYWRIGHT_BROWSERS_PATH / chmod dance is needed. The exact block is in the reference
next to the canonical one; both keep PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 and the pin.
Playwright launches Chromium without its sandbox by default (chromiumSandbox defaults to
false, https://playwright.dev/docs/api/class-browsertype), which is why it starts in the
container at all: the container has NET_ADMIN but not SYS_ADMIN, so Chromium's setuid
sandbox could not initialize. No --no-sandbox flag is needed. To keep the browser sandbox
on host runs, turn it on outside the container only:
// playwright.config.ts — the sandbox Dockerfile sets ENV DEVCONTAINER=true
launchOptions: { chromiumSandbox: !process.env.DEVCONTAINER },UNVERIFIED: a host run with chromiumSandbox: true has not been measured yet.
This is the same build-time-vs-runtime-network pattern as the Rust/Haskell toolchains in
claude-code-docker-sandbox: compiler at build time = no allowlist entry; deps at runtime
= allowlist needed.
Trap 1: the rate-limit binding hangs every local request
Symptom: wrangler dev logs Ready and the port accepts TCP, but every request hangs
with no response; the startup log says connected to remote resource.
Wrangler 3.x wires rate limiting via the unsafe binding form, which wrangler dev cannot
simulate: the dev-time proxy to the remote Cloudflare resource never completes its
handshake in a credential-free environment and blocks the whole pipeline. The v4
top-level ratelimits key does not do this — it is simulated locally (verified 2026-09-02
in kokemusu, wrangler 4.125.0, credential-free container, under both vite dev and
wrangler dev --config dist/…: a 40-request burst against a 30/60 s limiter answered
29×200 + 11×429, no hang, no connected to remote resource line). Fix for the 3.x form,
and still the recommendation for v4: strip both keys from the built config before
serving it for e2e — the limiter is fail-open, rate limiting is out of e2e scope (verify it
via cloudflare-workers-bot-scan-defense),
and a stripped binding means a burst of e2e requests can never 429 your own golden path:
// e2e/prepare-config.ts — post-build, editing dist/<bundle>/wrangler.json
const cfg = JSON.parse(readFileSync(CONFIG, "utf8"));
delete cfg.unsafe; // wrangler 3.x form (observed to hang credential-free)
delete cfg.ratelimits; // wrangler 4.x form: simulated locally since ≤ 4.125 (verified
// 2026-09-02); stripped anyway so e2e never depends on the limiterWhile you are in that file: @cloudflare/vite-plugin 1.x copies .dev.vars into
dist/<worker>/ at build time, and wrangler reads .dev.vars from the directory of the
config it was given — so the developer's DEV_CSP=1 / ORIGIN=…:5273 silently override
whatever vars the config carries (verified 2026-09-02: relaxed CSP and 403s on every POST
until it was removed). Either rmSync("dist/<worker>/.dev.vars") here and write the e2e
values into cfg.vars, or pass every key with --var (CLI --var outranks the copy —
verified with --var RP_ID:vartest showing up in login/begin). The main skill's Trap 2
has the full write-up.
Trap 2: bind --ip 127.0.0.1, not localhost
Symptom: wrangler dev is "Ready", TCP connects, but the Worker never returns a byte —
no CSP error, no SQLite error, just a stall. Bound to localhost (the dev.ip default),
routing stalls on IPv4/IPv6 resolution in the container (localhost → both 127.0.0.1
and ::1). Fix: use 127.0.0.1 literally end to end — the bind, ORIGIN, and
Playwright baseURL must all agree (a host/ORIGIN mismatch also 403s mutations via the
CSRF check):
"e2e:server": "… wrangler dev --config dist/<bundle>/wrangler.json --persist-to .wrangler/state --ip 127.0.0.1 --port 5399"Also pin dev.ip in the built config (prepare-config.ts:
cfg.dev = { ...cfg.dev, ip: "127.0.0.1" }) so a stray flag-less wrangler dev stays
consistent. When e2e hangs at connect-but-no-response, these two traps are the suspects —
rule out the bind address first (cheaper to check).
Scope boundary — what this skill does NOT cover
- The e2e suite wiring itself (build-artifact serving,
--persist-tostate trap, WebAuthn / OAuth seams, 3-spec scope) —cloudflare-workers-e2e-playwright - The sandbox environment itself (firewall, compose files, allowlist tuning) —
claude-code-docker-sandbox - Verifying rate-limiting behavior (deliberately stripped here) —
cloudflare-workers-bot-scan-defense
References
- references/in-container-playwright-bake.md — the canonical Dockerfile block, the five details that matter, bake verification, and both traps in full diagnostic detail
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

