diff --git a/claude-code/.devcontainer/Dockerfile b/claude-code/.devcontainer/Dockerfile index 43a73b2..e6bec7e 100644 --- a/claude-code/.devcontainer/Dockerfile +++ b/claude-code/.devcontainer/Dockerfile @@ -45,7 +45,6 @@ RUN set -eux; \ FROM node:24-trixie-slim AS base ARG GIT_DELTA_VERSION=0.18.2 -ARG PLAYWRIGHT_VERSION=1.58.2 # System packages shared by all targets # - ca-certificates: SSL/TLS for HTTPS connections @@ -115,14 +114,15 @@ USER root SHELL ["/bin/bash", "-o", "pipefail", "-c"] RUN curl -sS https://starship.rs/install.sh | sh -s -- --yes RUN curl https://mise.run | sh \ - && cp /root/.local/bin/mise /usr/local/bin/mise + && cp /root/.local/bin/mise /usr/local/bin/mise \ + && rm -rf /root/.local SHELL ["/bin/sh", "-c"] # Configure starship and fish shell. # Mise is installed as a tool manager — projects run `mise install` at container # creation to install their specific tool versions from .mise.toml. -# Node is NOT installed via mise — it's provided by the base image; -# mise shims would shadow the base image's npx, breaking Playwright installation. +# Node is NOT installed via mise — it's provided by the base image; mise shims +# would shadow the base image's npx, breaking global npm-installed CLIs. USER node RUN mkdir -p /home/node/.config/fish \ && starship preset no-runtime-versions -o /home/node/.config/starship.toml \ @@ -137,22 +137,6 @@ ENV MISE_TRUSTED_CONFIG_PATHS="/workspace" COPY --from=rtk-download /usr/local/bin/rtk /usr/local/bin/rtk COPY --from=ralphex-download /usr/local/bin/ralphex /usr/local/bin/ralphex -# ── Playwright (headless shell for browser testing) ─────────────────────────── -# Two-layer strategy for multi-project compatibility: -# 1. System deps (install-deps) — heavy, needs root, version-stable across -# nearby releases. Baked into the image so projects don't need sudo. -# 2. Browser binary (install --only-shell) — light, version-specific. -# Baked as a cache; projects MUST run `npx playwright install --only-shell chromium` -# at container creation to ensure the binary matches their @playwright/test. -# That command is idempotent: no-op when versions match, ~10s download if not. -USER root -RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ - --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ - npx -y playwright@${PLAYWRIGHT_VERSION} install-deps chromium -USER node -RUN npx -y playwright@${PLAYWRIGHT_VERSION} install --only-shell -ENV PLAYWRIGHT_VERSION=${PLAYWRIGHT_VERSION} - # ── Claude Code CLI ─────────────────────────────────────────────────────────── # Using npm instead of the native installer (curl claude.ai/install.sh | bash). # The native installer is recommended for interactive use, but rate-limits (429) @@ -161,10 +145,11 @@ ENV PLAYWRIGHT_VERSION=${PLAYWRIGHT_VERSION} # reasons" — Docker/CI parallel builds are exactly that reason. # See: https://code.claude.com/docs/en/getting-started#install-with-npm # Auto-updates don't matter here — the image rebuilds daily. -USER root -RUN --mount=type=cache,target=/root/.npm \ - npm install -g @anthropic-ai/claude-code -USER node +# +# Install as the node user so all files land with node ownership from the start. +# A later `chown -R` in another layer would duplicate every file (overlayfs +# treats an ownership change as a rewrite), adding hundreds of MB. +RUN npm install -g @anthropic-ai/claude-code # ─── DEFAULT — full dev environment ─────────────────────────────────────────── FROM base AS default @@ -177,24 +162,32 @@ RUN echo "node ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/node-nopasswd \ && chmod 0440 /etc/sudoers.d/node-nopasswd USER node -# agent-browser: headless browser automation for AI agents -# amd64: Chrome for Testing (Google's official automation channel) -# arm64: system Chromium (Chrome for Testing has no ARM64 Linux builds) -ARG AGENT_BROWSER_VERSION=latest +# System Chromium + fonts for headless browser testing. +# - chromium: used by Playwright and agent-browser via +# PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH (see .claude/rules/dockerfile.md). +# - fonts-freefont-ttf: baseline fonts for headless rendering. +# Kept out of the base stage so the sandbox target stays lean. USER root -RUN --mount=type=cache,target=/root/.npm \ - --mount=type=cache,target=/var/cache/apt,sharing=locked \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ - npm install -g agent-browser@${AGENT_BROWSER_VERSION} \ - && ARCH=$(dpkg --print-architecture) \ - && if [ "$ARCH" = "amd64" ]; then \ - agent-browser install --with-deps; \ - else \ - apt-get update && apt-get install -y --no-install-recommends chromium; \ - fi \ - && chown -R node:node /usr/local/share/npm-global + apt-get update && apt-get install -y --no-install-recommends \ + chromium \ + fonts-freefont-ttf USER node +# Use the apt-installed chromium instead of Playwright-managed browsers. +# Avoids version coupling between @playwright/mcp (alpha playwright-core builds) +# and cached browser binaries. Projects using @playwright/test must point their +# playwright.config.ts at process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH. +ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \ + PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium + +# agent-browser: headless browser automation for AI agents. +# Uses the apt-installed chromium above. +# Installed as the node user (see claude-code install above for rationale). +ARG AGENT_BROWSER_VERSION=latest +RUN npm install -g agent-browser@${AGENT_BROWSER_VERSION} + LABEL org.opencontainers.image.source="https://github.com/gatezh/devcontainers" \ org.opencontainers.image.description="Claude Code devcontainer — full dev environment with agent-browser, Playwright, and passwordless sudo" \ org.opencontainers.image.licenses="MIT" \ diff --git a/claude-code/.devcontainer/claude-sandbox/devcontainer.json b/claude-code/.devcontainer/claude-sandbox/devcontainer.json index 46189b3..1281a84 100644 --- a/claude-code/.devcontainer/claude-sandbox/devcontainer.json +++ b/claude-code/.devcontainer/claude-sandbox/devcontainer.json @@ -120,9 +120,10 @@ // injected from the host. See "Sandbox Authentication" section in README. "CLAUDE_CODE_OAUTH_TOKEN": "${localEnv:CLAUDE_CODE_OAUTH_TOKEN}" }, - // Runs before postStartCommand (firewall), so network is still available for browser downloads. // The find command chowns all node_modules volume mount points in one pass. - "postCreateCommand": "sudo find /workspace -maxdepth 4 -name node_modules -type d -exec chown node {} + && sudo chown -R node /home/node/.claude && mise install && bun install && npx playwright install --only-shell chromium", + // Note: the sandbox image does not include chromium — projects needing + // browser testing should install it via a downstream Dockerfile. + "postCreateCommand": "sudo find /workspace -maxdepth 4 -name node_modules -type d -exec chown node {} + && sudo chown -R node /home/node/.claude && mise install && bun install", // Firewall init — script is bind-mounted from the project "postStartCommand": "sudo /usr/local/bin/init-firewall.sh", "waitFor": "postStartCommand" diff --git a/claude-code/.devcontainer/devcontainer.json b/claude-code/.devcontainer/devcontainer.json index cd40704..6fdb804 100644 --- a/claude-code/.devcontainer/devcontainer.json +++ b/claude-code/.devcontainer/devcontainer.json @@ -108,8 +108,8 @@ // sudo chown fixes volume ownership — safety net in case Docker volume population didn't apply. // The find command chowns all node_modules volume mount points in one pass. // mise install reads .mise.toml and installs project-specific tool versions. - // playwright install ensures the correct browser binary for the project's @playwright/test version - // (idempotent — skips download if the image's cached binary already matches). - "updateContentCommand": "sudo find /workspace -maxdepth 4 -name node_modules -type d -exec chown node {} + && sudo chown -R node /home/node/.claude && mise install && bun install && npx playwright install --only-shell chromium", + // Chromium is baked into the image via apt — no playwright install step needed. + // Projects' playwright.config.ts should use process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH. + "updateContentCommand": "sudo find /workspace -maxdepth 4 -name node_modules -type d -exec chown node {} + && sudo chown -R node /home/node/.claude && mise install && bun install", "waitFor": "postCreateCommand" } diff --git a/claude-code/README.md b/claude-code/README.md index d41a21a..4ccc618 100644 --- a/claude-code/README.md +++ b/claude-code/README.md @@ -21,9 +21,8 @@ Projects consume these pre-built images and control their own tool versions via | Mise | The tool manager itself (not the tools) | Projects run `mise install` at container creation for their tool versions | | rtk, ralphex | Always-latest from GitHub Releases | Dev infrastructure (like Claude Code) — no version pinning needed in projects | | Claude Code | npm global install | npm avoids rate limiting that affects the native installer in parallel CI builds | -| Playwright | System deps + browser binary at a pinned version | See [Playwright version strategy](#playwright-version-strategy) | -**Default-only:** passwordless sudo, agent-browser + full Chromium +**Default-only:** passwordless sudo, agent-browser, system Chromium (used by Playwright via `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH`) **Sandbox-only:** iptables, ipset, iproute2, dnsutils, aggregate, firewall sudo rule @@ -56,7 +55,7 @@ Copy these to your project's `.devcontainer/`: - [`.devcontainer/docker-compose.yml`](.devcontainer/docker-compose.yml) — image reference with `pull_policy: always` - [`.devcontainer/devcontainer.json`](.devcontainer/devcontainer.json) — full config with VS Code extensions, fish shell, OXC formatter, node_modules volume isolation, and lifecycle commands -**Key settings included:** fish + bash terminal profiles, OXC formatter (with comments for switching to Biome/Prettier), node_modules/Claude config/fish history volume mounts, and `updateContentCommand` for mise/bun/Playwright setup. +**Key settings included:** fish + bash terminal profiles, OXC formatter (with comments for switching to Biome/Prettier), node_modules/Claude config/fish history volume mounts, and `updateContentCommand` for mise/bun setup. ### Sandbox variant @@ -212,37 +211,46 @@ The template only mounts root `node_modules` by default. For monorepo projects, The `sudo find` in `updateContentCommand` chowns all `node_modules` directories in one pass, so additional mounts are handled automatically. -## Playwright Version Strategy +## Playwright Strategy -The image uses a two-layer strategy so that any project can use any `@playwright/test` version: +The image uses the system `chromium` package (apt-installed in the `default` +target) instead of Playwright-managed browser binaries. This avoids version +coupling between `@playwright/mcp` (alpha `playwright-core` builds) and cached +binaries, and keeps the image significantly smaller than shipping a +Playwright-managed Chromium per rebuild. -| Layer | What | Baked in image? | Version-sensitive? | -|-------|------|-----------------|--------------------| -| System deps (`install-deps`) | OS libraries (libgbm, libnss3, …) | Yes (needs root) | Stable across nearby versions | -| Browser binary (`install --only-shell`) | Headless Chromium shell | Yes, as cache | Exact match required | - -**Prerequisite:** Your project must have `@playwright/test` in `devDependencies`. Without it, `npx` will prompt interactively to download the `playwright` package, which hangs container creation (no TTY). Pin it to the image's baked version (check `$PLAYWRIGHT_VERSION`) or your preferred version: +The Dockerfile sets: ```bash -bun add -d @playwright/test # or: npm install -D @playwright/test +PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 +PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium ``` -**One-time project setup** — add this to your container creation command (`updateContentCommand` or `postCreateCommand`, as shown in [Quick Start](#quick-start)): - -```bash -npx playwright install --only-shell chromium +`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` is a project convention — Playwright +does **not** read it automatically. Each project's `playwright.config.ts` +must honor it: + +```ts +// playwright.config.ts +export default defineConfig({ + use: { + launchOptions: { + executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH, + }, + }, +}); ``` -This is **idempotent** — if the cached binary already matches, it's a no-op (~0s). If the project's `@playwright/test` version differs from the image, it downloads the correct binary (~10s, once at container creation). - -The baked version is available at runtime via the `PLAYWRIGHT_VERSION` environment variable. +**Sandbox note:** `chromium` is installed only in the `default` target. +Projects using the `sandbox` image that need browser testing should add +chromium themselves (via a thin downstream Dockerfile or container +`postStart`). ## Build Args | Arg | Default | Description | |-----|---------|-------------| | `GIT_DELTA_VERSION` | `0.18.2` | git-delta version | -| `PLAYWRIGHT_VERSION` | `1.58.2` | Playwright browser binary version | | `AGENT_BROWSER_VERSION` | `latest` | agent-browser version (default target only) | ## Building Locally / Local Fallback @@ -311,25 +319,16 @@ docker buildx build \ ## Startup Timeline -### Warm start (Playwright version matches image) ``` Pull image ──────────────────────── (cached) mise install (bun, hugo, etc.) ──── (~15s, downloads pre-built binaries) bun install ─────────────────────── (~15s, cached in named volume) -playwright install --only-shell chromium (~0s, binary already cached — no-op) project setup ───────────────────── (db:migrate, init-plugins, etc.) Total: ~45s warm ``` -### Playwright version mismatch -``` -Pull image ──────────────────────── (cached) -mise install (bun, hugo, etc.) ──── (~15s) -bun install ─────────────────────── (~15s) -playwright install --only-shell chromium (~10s, downloads correct browser binary) -project setup ───────────────────── (db:migrate, init-plugins, etc.) - Total: ~55s warm -``` +Chromium is baked into the image via apt — no per-container browser +download step required. ## Resources