#ddev-generated # Opt-in headless browser for Claude Code: Playwright MCP with headless Chromium. # # This is the template. It does nothing where it is: "ddev claude-browser enable" # copies it to .ddev/docker-compose.claude-browser.yaml and restarts the project, and # install.sh registers it with Claude Code as the "playwright" MCP server # (http://claude-browser:8931/mcp). While the copy keeps the #ddev-generated line, # install.sh refreshes it from this template after add-on upgrades; remove that line # from the copy to customise it. # # Pick another release with CLAUDE_BROWSER_IMAGE in .ddev/.env, e.g. # CLAUDE_BROWSER_IMAGE=mcr.microsoft.com/playwright/mcp:v0.0.84 services: claude-browser: image: ${CLAUDE_BROWSER_IMAGE:-mcr.microsoft.com/playwright/mcp:v0.0.83}-${DDEV_SITENAME}-built build: # The upstream image runs as "node" (uid 1000). Turn that user into the host user # (same name, uid and gid) so screenshots and other files the browser writes into # the mounted project belong to you. Same approach as ddev-selenium-standalone-chrome. dockerfile_inline: | ARG CLAUDE_BROWSER_IMAGE="scratch" FROM $${CLAUDE_BROWSER_IMAGE} USER root ARG username ARG uid ARG gid RUN set -eu; \ old_uid="$$(id -u node)"; old_gid="$$(id -g node)"; \ for u in "$$(getent passwd "$${uid}" | cut -d: -f1)" "$$(getent passwd "$${username}" | cut -d: -f1)"; do \ if [ -n "$${u}" ] && [ "$${u}" != "node" ]; then userdel "$${u}"; fi; \ done; \ for g in "$$(getent group "$${gid}" | cut -d: -f1)" "$$(getent group "$${username}" | cut -d: -f1)"; do \ if [ -n "$${g}" ] && [ "$${g}" != "node" ]; then groupdel "$${g}" || true; fi; \ done; \ groupmod -o -g "$${gid}" node; \ if ! getent group "$${username}" >/dev/null; then groupmod -n "$${username}" node; fi; \ usermod -o -u "$${uid}" -g "$${gid}" node; \ if [ "$${username}" != "node" ]; then usermod -l "$${username}" node; fi; \ find / -xdev -uid "$${old_uid}" -exec chown -h "$${uid}" {} +; \ find / -xdev -gid "$${old_gid}" -exec chgrp -h "$${gid}" {} + USER $${uid}:$${gid} args: CLAUDE_BROWSER_IMAGE: ${CLAUDE_BROWSER_IMAGE:-mcr.microsoft.com/playwright/mcp:v0.0.83} username: ${DDEV_USER} uid: ${DDEV_UID} gid: ${DDEV_GID} container_name: ddev-${DDEV_SITENAME}-claude-browser user: "${DDEV_UID}:${DDEV_GID}" restart: "no" # Chromium needs more than Docker's 64 MB default /dev/shm for heavier pages. shm_size: 1gb # Appended to the image's entrypoint (node cli.js --headless --browser chromium --no-sandbox). command: - --port=8931 - --host=0.0.0.0 # The port is only exposed on the project's Docker network, never published to # the host, so the Host-header check adds nothing here. - --allowed-hosts=* # DDEV's mkcert CA is not in this container's trust store. - --ignore-https-errors # Fresh in-memory profile per session, so parallel "ddev claude" sessions never # fight over one profile directory. - --isolated # Same path as in the web container, so paths in tool results work for Claude as-is. - --output-dir=/var/www/html/.ddev/claude-code/browser-output - --file-paths=absolute expose: - 8931 volumes: # The project at the web container's path: file uploads and saved screenshots # use paths Claude can read directly. - "../:/var/www/html" x-ddev: describe-info: | Playwright MCP: http://claude-browser:8931/mcp Toggle with: ddev claude-browser enable|disable