agentleFS
okayusokayus/okayus-skills/skills/playwright-e2e-in-docker-sandbox/SKILL.md

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

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
  1. Very specific:

    If you develop in a network-isolated Docker sandbox (egress firewall, no host browser, no Cloudflare login — the claude-code-docker-sandbox …

    line 13
  2. Distinctive:

    Do not use for:

    line 39

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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 install can't work
  • pnpm e2e in-container fails with browser not found
  • A credential-free wrangler dev (sandbox, or a logged-out host) logs Ready and 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 lost and wrangler dev exits — not a sandbox trap: see Trap 3 of cloudflare-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's cloudflare-workers-e2e-playwright
  • Non-sandboxed environments — a plain playwright install chromium on the host is fine

Deliverables (completion criteria)

  • Browser baked at image-build time via the INSTALL_PLAYWRIGHT build arg — zero runtime egress, no init-firewall.sh change
  • ARG PLAYWRIGHT_VERSION exactly equals @playwright/test in package.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 (unsafe and ratelimits — the v4 key no longer hangs, see Trap 1, but stripping keeps e2e independent of the limiter)
  • The .dev.vars copy that @cloudflare/vite-plugin 1.x writes into dist/<worker>/ is neutralised — deleted by prepare-config, or every key overridden with --var
  • wrangler dev binds --ip 127.0.0.1 (not localhost), and ORIGIN + Playwright baseURL use 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 image

Key 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 limiter

While 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-to state 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

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.