This container is based on: https://code.claude.com/docs/en/devcontainer and https://github.com/anthropics/claude-code/tree/main/.devcontainer but somewhat heavily modified.
Make a folder with a workspace folder inside it:
mkdir -p claude-container-instance/workspace claude-container-instance/dot-claude claude-container-instance/m2-cache
go into the folder and create a .env file
cd claude-container-instance
nano .env
in the env file copy the following (.env.example):
# Claude Container Environment Variables
# Copy this file to .env and customize as needed
# Usage: docker run --env-file .env ...
# Claude configuration directory (inside container)
CLAUDE_CONFIG_DIR=/home/node/.claude
# GitHub Personal Access Token for gh CLI
# GH_TOKEN=ghp_your_token_here
# Git identity (used for commits inside the container)
# GIT_USER_NAME=Your Name
# GIT_USER_EMAIL=your.email@example.com
# Timezone (defaults to Europe/Helsinki if not set)
# TZ=America/New_York
# Skip firewall initialization (set to 1 to disable)
SKIP_FIREWALL=1
# Playwright browsers path (pre-installed in image)
PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers
# Skip Playwright browser downloads (browsers are pre-installed)
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
# Notification URL (e.g., ntfy.sh) - called when Claude is idle or needs permission
# Supports any URL that accepts POST requests (ntfy.sh, webhooks, etc.)
# NOTIFICATION_URL=https://ntfy.sh/your-topic-here
# Vaadin Pro/Commercial key (for Charts, Board, Acceleration Kits, etc.)
# Alternatively, mount a ~/.vaadin/proKey file into the container
# VAADIN_PRO_KEY=your-pro-key-here
# Node.js memory limit
NODE_OPTIONS=--max-old-space-size=4096
For a first test the above config disables the firewall, enables chrome CDP debuggin on port 9222 (if enabled inside the container etc).
- If you want to commit inside the container, you probably want to set the
GIT_USERrelated params - If you want to use Vaadin commercial components, either set your
proKey(just the value starting withpro-in the environment variable, or later inside the contaiermkdir -p ~/.vaadin && nano ~/node/.vaadin/proKeyand copy your entire prokey from a different system (or follow the browser process to sign in...) GH_TOKENis there if you want to use Github fine graned PATs for limited access to GH from within the container- etc..
then run the prebuilt container with (caches the .m2 folder on the host for less downloads on second try):
docker run -it --rm \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
--env-file .env \
-v "./dot-claude:/home/node/.claude" \
-v "./m2-cache:/home/node/.m2" \
-v "./workspace:/workspace" \
-p 9222:9222 \
ghcr.io/petrixh/claude-container:latest
Clone your projects under /workspace, run claude do stuff :)
Read what the entrypoint.sh command tells you about versions if wou want to use built-in predownloaded browers, playwright MCP in headless mode etc. Or don't... you can re-run the entrypoint inside the container terminal by just typing entrypoint.sh to get to the info later also (should be fine to rerun). To update claude run sudo claude update as new versions are being pushed constantly...
TL;DR I just want a devcontainer from my IDE/Codespaces optinally with docker-in-docker fully understanding the risks it might bring
Start by checking out your project in a directory, then inside that directory, minimally you only need the .devcontainer directory, but the others reduce future downloads, preserves things etc.
mkdir .devcontainer dot-claude m2-cache
then create a deccontainer file under .devcontainer/devcontainer.json with the content:
{
"name": "Claude Code Sandbox",
"image": "ghcr.io/petrixh/claude-container:latest",
"runArgs": [
"--cap-add=NET_ADMIN",
"--cap-add=NET_RAW",
"--env-file=.devcontainer/.env", //if desired to setup vars in a file instead, comment out containerEnv-entries if used
],
// If you want to bring env variables from your current environment to the devconatiner
// "containerEnv": {
// "SKIP_FIREWALL": "1",
// "GH_TOKEN": "${localEnv:GH_TOKEN}",
// "GIT_USER_NAME": "${localEnv:GIT_USER_NAME}",
// "GIT_USER_EMAIL": "${localEnv:GIT_USER_EMAIL}",
// "NODE_OPTIONS": "--max-old-space-size=4096",
// "CLAUDE_CONFIG_DIR": "/home/node/.claude",
// "PLAYWRIGHT_BROWSERS_PATH": "/opt/playwright-browsers",
// "PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD": "1",
// //"VAADIN_PRO_KEY": "${localEnv:VAADIN_PRO_KEY}",
// "NOTIFICATION_URL": "${localEnv:NOTIFICATION_URL}",
// },
"customizations": {
"vscode": {
"extensions": [
"vscjava.vscode-java-pack",
"vscjava.vscode-maven"
],
"settings": {
"terminal.integrated.defaultProfile.linux": "zsh",
"editor.formatOnSave": true,
"java.configuration.detectJdksAtStart": true
}
}
},
// If you need docker inside the dev container, does open up options for the AI to do all kinds of things...
// "features": {
// "ghcr.io/devcontainers/features/docker-in-docker:2": {
// "version": "latest",
// "enableNonRootDocker": "true",
// "moby": "false"
// }
// },
"remoteUser": "node",
"mounts": [
"source=./m2-cache,target=/home/vscode/.m2,type=bind,consistency=cached",
// "source=claude-code-bashhistory-${devcontainerId},target=/commandhistory,type=volume",
"source=./dot-claude,target=/home/node/.claude,type=bind,consistency=cached"
],
"forwardPorts": [
9222
],
"portsAttributes": {
"9222": {
"label": "Playwright CDP",
"onAutoForward": "silent"
}
},
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated",
"workspaceFolder": "/workspace",
"postStartCommand": "/usr/local/bin/entrypoint.sh"
}
create an .env file under .devcontainer and add it to .gitignore immediatlely! Or create it somewhere else and map it at the top in the runArgs section. Inside the .env file look at .env.example or just copy paste:
# Claude Container Environment Variables
# Copy this file to .env and customize as needed
# Usage: docker run --env-file .env ...
# Claude configuration directory (inside container)
CLAUDE_CONFIG_DIR=/home/node/.claude
# GitHub Personal Access Token for gh CLI
# GH_TOKEN=ghp_your_token_here
# Git identity (used for commits inside the container)
# GIT_USER_NAME=Your Name
# GIT_USER_EMAIL=your.email@example.com
# Timezone (defaults to Europe/Helsinki if not set)
# TZ=America/New_York
# Skip firewall initialization (set to 1 to disable)
SKIP_FIREWALL=1
# Playwright browsers path (pre-installed in image)
PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers
# Skip Playwright browser downloads (browsers are pre-installed)
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
# Notification URL (e.g., ntfy.sh) - called when Claude is idle or needs permission
# Supports any URL that accepts POST requests (ntfy.sh, webhooks, etc.)
# NOTIFICATION_URL=https://ntfy.sh/your-topic-here
# Vaadin Pro/Commercial key (for Charts, Board, Acceleration Kits, etc.)
# Alternatively, mount a ~/.vaadin/proKey file into the container
# VAADIN_PRO_KEY=your-pro-key-here
# Node.js memory limit
NODE_OPTIONS=--max-old-space-size=4096
If you had the folder open in VS Code, it should have already prompted you that a devcotnainer config was found. If not open the command palette and with > at the front look for "Dev Containers: Rebuild and Reopend in Container". It will download the internet and reopen the folder inside the devcotnaienr in VS Code.
Read what the entrypoint.sh command tells you about versions if wou want to use built-in predownloaded browers, playwright MCP in headless mode etc. Or don't... you can re-run the entrypoint inside the container terminal by just typing entrypoint.sh to get to the info later also (should be fine to rerun). To update claude run sudo claude update as new versions are being pushed constantly...
mkdir workspace
Then clone your project(s) under workspace
After starting the devconatiner in VS Code use the command palette (top center) and with > at the beginning look for "File: Open Folder" and then navigate to /workspace/workspace/your-project. Now your devcontainer config and project are separate, but all still runs inside the devcontainer. To get back to your devcontainer config do the same open but select /workspace instead.
To keep your devcontainer and workspace permananently separate change the workspaceMount entry, use: -v "${localWorkspaceFolder}/workspace/:/workspace" for the bind mount instead.
Below is a more compehensive example with instructions etc... Reading through them can give you insight into the whats and the whys...
A Docker devcontainer for running Claude Code in a sandboxed environment with:
- Node.js 20
- Java 21 (Eclipse Temurin)
- Playwright with headless Chromium (arm64/amd64)
- GitHub CLI with PAT authentication
- Domain-whitelist firewall (default deny)
- Configurable Claude config directory for subscription authentication
- Optional Docker-in-Docker (DinD) support for containerized development
Before running the container, ensure:
- Claude config directory exists on your Docker host for persisting authentication:
mkdir -p /path/to/claude-container-config
- GitHub PAT (optional) - set the
GH_TOKENenvironment variable:export GH_TOKEN=ghp_your_token_here
This repository provides three container variants:
| Variant | Size | Docker | Best For | Limitations |
|---|---|---|---|---|
claude (base) ⭐ |
3.47GB | ❌ No | General Claude Code development | No Docker support |
claude-docker-host |
3.92GB | ✅ Via host | Docker development, testing | Requires host Docker |
claude-dind |
3.92GB | ✅ Isolated | Secure isolation, CI/CD | Firewall blocks Docker Hub |
Choose claude (base) if: ⭐ RECOMMENDED
- General development (Java, Node.js, etc.)
- You don't need Docker inside the container
- You want the smallest, fastest container
Choose claude-docker-host if:
- You need Docker and have Docker on your host
- You want to pull from Docker Hub
- You want lower resource usage than DinD
Choose claude-dind if:
- You need complete isolation from host Docker
- You're testing untrusted code
- You're willing to work around firewall limitations
Standard Development (recommended):
docker compose up -d claude
docker compose exec claude zsh
claude --versionDocker Development (host socket):
docker compose up -d claude-docker-host
docker compose exec claude-docker-host zsh
docker pull alpine # Works without firewall issues!Isolated Docker Environment:
docker compose up -d claude-dind
docker compose exec claude-dind zsh
# Note: Docker Hub pulls blocked by firewall - see Known Limitations# Base variant (default)
docker build -t claude-container:base --target base .devcontainer/
# DinD variant (both claude-dind and claude-docker-host use this)
docker build -t claude-container:dind --target dind .devcontainer/Start an interactive development environment:
# Start container in background and attach
docker compose up -d && docker compose exec claude zsh
# Or start and attach directly (container removed on exit)
docker compose run --rm claude
# Stop the container
docker compose down
# DinD variant with separate Docker daemon
docker compose up -d claude-dind && docker compose exec claude-dind zsh
# DinD variant mounting host Docker socket
docker compose up -d claude-docker-host && docker compose exec claude-docker-host zshdocker run -it --rm \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
-v "/path/to/claude-container-config:/home/node/.claude" \
-e CLAUDE_CONFIG_DIR="/home/node/.claude" \
-w /workspace \
claude-container:baseNote: The
CLAUDE_CONFIG_DIRenvironment variable tells Claude where to store authentication credentials. Mount a host directory to persist login across container restarts.
For easier management of environment variables, use a .env file:
# Copy the example env file and edit with your settings
cp .env.example .env
vim .env
# Minimal: just env file
docker run -it --rm \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
--env-file .env \
claude-container:baseAdd Maven cache to avoid re-downloading dependencies:
mkdir -p ~/.m2-cache
docker run -it --rm \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
--env-file .env \
-v "${HOME}/.m2-cache:/home/node/.m2" \
claude-container:baseAdd workspace to work on a project:
docker run -it --rm \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
--env-file .env \
-v "${HOME}/.m2-cache:/home/node/.m2" \
-v "$(pwd):/workspace" \
claude-container:baseFull setup with all common mounts:
docker run -it --rm \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
--env-file .env \
-v "${HOME}/.claude:/home/node/.claude" \
-v "${HOME}/.m2-cache:/home/node/.m2" \
-v "${HOME}/.npm-cache:/home/node/.npm" \
-v "${HOME}/.config/gh:/home/node/.config/gh" \
-v "$(pwd):/workspace" \
-p 9222:9222 \
claude-container:baseVolume mounts explained:
| Host Path | Container Path | Purpose |
|---|---|---|
~/.claude |
/home/node/.claude |
Claude authentication and config |
~/.m2-cache |
/home/node/.m2 |
Maven local repository cache |
~/.npm-cache |
/home/node/.npm |
NPM cache |
~/.config/gh |
/home/node/.config/gh |
GitHub CLI authentication |
$(pwd) |
/workspace |
Your project directory |
Execute a single prompt and exit:
docker run -it --rm \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
-v "/path/to/claude-container-config:/claude-container-config" \
-e CLAUDE_CONFIG_DIR="/claude-container-config" \
-w /workspace \
claude-container \
claude -p "Your prompt here"- Open this repository in VS Code
- Install the "Dev Containers" extension
- Run "Dev Containers: Reopen in Container" from the command palette
Use the alternative configuration for Docker-in-Docker support:
# Using devcontainer CLI
devcontainer up --workspace-folder . \
--config .devcontainer/devcontainer-dind.json
# Or rename the config (backup original first)
mv .devcontainer/devcontainer.json .devcontainer/devcontainer-base.json
mv .devcontainer/devcontainer-dind.json .devcontainer/devcontainer.json
⚠️ Important: Theclaude-dindvariant's firewall blocks Docker Hub image pulls due to dynamic CDN domains. For Docker development with image pulling, use theclaude-docker-hostvariant instead. See Known Limitations for details.
Base Variant (claude) ⭐ Recommended
- ✅ Standard Claude Code development
- ✅ Smallest image size (3.47GB)
- ✅ Fastest startup (~2 seconds)
- ✅ Lowest resource usage
- Use when: General development without Docker needs (most users)
Host Socket Variant (claude-docker-host) - For Docker development
- ✅ Access to Docker without firewall issues
- ✅ Shares host Docker daemon and images
- ✅ Lower resource usage than separate daemon
- ✅ No privileged mode required
⚠️ Can affect host Docker state⚠️ Requires host Docker installation- Use when: You need Docker and trust your environment
Separate Daemon Variant (claude-dind) - For isolated environments
- ✅ Full isolation from host Docker
- ✅ Persistent Docker cache in container
- ✅ Works without host Docker
⚠️ Requires privileged mode⚠️ Higher resource usage (+100-200MB RAM)⚠️ Docker Hub pulls blocked by firewall⚠️ Slower startup (~8 seconds)- Use when: You need isolated Docker or untrusted code testing
# Enter DinD container
docker compose exec claude-dind zsh
# Verify Docker installation
docker version
docker compose version
# Note: Docker Hub image pulls may be blocked by firewall
# See "Known Limitations" section below for workaroundsIssue: Docker Hub now uses Cloudflare R2 storage with dynamic subdomains that cannot be whitelisted by domain name. Image pulls will timeout when the firewall is enabled.
Symptoms:
failed to do request: Get "https://docker-images-prod.*.r2.cloudflarestorage.com/...":
dial tcp 172.64.66.1:443: i/o timeout
Workarounds:
Option 1: Disable Firewall for DinD Container
# Method A: Skip firewall initialization
docker run -it --rm --privileged \
-e SKIP_FIREWALL=1 \
claude-container:dind
# Method B: Run with permissive OUTPUT policy
docker run -it --rm --privileged \
claude-container:dind bash -c \
"sudo iptables -P OUTPUT ACCEPT && exec zsh"Option 2: Use Host Docker Socket (Recommended)
The claude-docker-host variant mounts the host's Docker socket, bypassing the firewall issue:
docker compose up -d claude-docker-host
docker compose exec claude-docker-host zsh
# Now use host's Docker (images, containers shared with host)
docker pull alpine
docker images # Shows host imagesOption 3: Pre-pull Images on Host
Pull images on your host machine, then they're available inside DinD:
# On host
docker pull alpine:latest
docker pull node:20
# In claude-docker-host container
docker images # Images available immediatelyOption 4: Use Local/Private Registry
Configure a local or private registry with known domain names:
# Add your registry to allowed-domains.conf
echo "registry.mycompany.com" >> .devcontainer/allowed-domains.conf
# Rebuild and use
docker pull registry.mycompany.com/myimage:latestOption 5: Build Images Locally
Build images inside the container without pulling from Docker Hub:
# Inside DinD container
cat > Dockerfile <<'EOF'
FROM scratch
COPY ./myapp /app
CMD ["/app/myapp"]
EOF
docker build -t myapp:latest .
docker run myapp:latestThe container uses a domain-whitelist firewall that blocks all outbound traffic except to approved domains. A default whitelist is baked into the image at /usr/local/etc/allowed-domains.conf.
api.anthropic.com- Claude APIregistry.npmjs.org- NPM packagesgithub.com,api.github.com- GitHubrepo1.maven.org,repo.maven.apache.org- Maven Centralplaywright.azureedge.net- Playwright browser downloads- VS Code marketplace domains
registry-1.docker.io,auth.docker.io- Docker Hub (DinD variant)
To use your own domain whitelist, bind mount a custom config file:
docker run -it --rm \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
-v "/path/to/your/allowed-domains.conf:/usr/local/etc/allowed-domains.conf:ro" \
-v "/path/to/claude-container-config:/claude-container-config" \
-e CLAUDE_CONFIG_DIR="/claude-container-config" \
claude-containerOr copy the default config and modify it:
# Copy from this repo
cp .devcontainer/allowed-domains.conf ~/my-allowed-domains.conf
# Edit to add your domains
echo "example.com" >> ~/my-allowed-domains.confInside the container:
# View current firewall rules
firewall-status
# Reload firewall after editing the config
firewall-reload| Variable | Description |
|---|---|
CLAUDE_CONFIG_DIR |
Directory for Claude authentication and config (mount from host for persistence) |
GH_TOKEN |
GitHub Personal Access Token for gh CLI authentication |
GIT_USER_NAME |
Git author/committer name (sets git config --global user.name) |
GIT_USER_EMAIL |
Git author/committer email (sets git config --global user.email) |
TZ |
Timezone (default: Europe/Helsinki). Pass -e TZ=$TZ to inherit from host |
CLAUDE_CODE_VERSION |
Claude Code version to install (default: latest) |
SKIP_FIREWALL |
Set to 1 to skip firewall initialization (useful for DinD troubleshooting) |
FIREWALL_CONFIG |
Custom path to allowed-domains.conf (default: workspace or /usr/local/etc) |
DOCKER_HOST |
Docker daemon socket (default: unix:///var/run/docker.sock) |
PLAYWRIGHT_BROWSERS_PATH |
Path to pre-installed Playwright browsers (default: /opt/playwright-browsers) |
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD |
Set to 1 to skip browser downloads (default: 1, browsers are pre-installed) |
NOTIFICATION_URL |
URL to POST notifications to when Claude is idle or needs permission (e.g., https://ntfy.sh/your-topic) |
VAADIN_PRO_KEY |
Vaadin Pro/Commercial subscription key for commercial components (Charts, Board, etc.) and Acceleration Kits |
The container can notify you when Claude needs attention via any URL that accepts POST requests (e.g., ntfy.sh, Slack webhooks, custom endpoints).
Set the NOTIFICATION_URL environment variable:
# In .env file
NOTIFICATION_URL=https://ntfy.sh/your-topic-here
# Or via docker run
docker run -it --rm \
-e NOTIFICATION_URL=https://ntfy.sh/your-topic-here \
claude-container:baseThe entrypoint automatically configures Claude Code hooks in settings.json for these events:
| Event | Message |
|---|---|
idle_prompt |
Claude finished and is waiting for your input (60+ seconds idle) |
permission_prompt |
Claude needs your permission to proceed |
elicitation_dialog |
Claude is asking a question and waiting for your answer |
You can manually add more hooks to ~/.claude/settings.json for these events:
| Matcher / Event | Description |
|---|---|
auth_success |
Authentication completed |
Stop (event, not matcher) |
Claude finished responding (fires every time) |
TaskCompleted (event) |
A task was marked as completed |
See the Claude Code hooks documentation for the full reference.
The container supports Vaadin commercial components (Charts, Board, Grid Pro, etc.) and Acceleration Kits via the VAADIN_PRO_KEY environment variable.
There are two ways to provide your Vaadin Pro subscription key:
Option 1: Environment variable (recommended for containers)
Set VAADIN_PRO_KEY in your .env file or pass it directly:
# In .env file
VAADIN_PRO_KEY=your-pro-key-here
# Or via docker run
docker run -it --rm \
-e VAADIN_PRO_KEY=your-pro-key-here \
claude-container:baseOption 2: Mount a proKey file
If you prefer file-based configuration, mount your ~/.vaadin/proKey file from the host:
docker run -it --rm \
-v "${HOME}/.vaadin:/home/node/.vaadin:ro" \
claude-container:baseIf you're using Vaadin commercial components with the firewall enabled, uncomment the Vaadin domains in allowed-domains.conf:
maven.vaadin.com
tools.vaadin.com
vaadin.com
cdn.vaadin.com
Or add them at runtime:
# Inside the container
echo -e "maven.vaadin.com\ntools.vaadin.com\nvaadin.com\ncdn.vaadin.com" | sudo tee -a /usr/local/etc/allowed-domains.conf
firewall-reloadThe container includes Playwright with Chromium pre-installed, ready to use with both Node.js and Java Playwright libraries.
The container includes two versions of Chromium to support different use cases:
- Standard Playwright Chromium (e.g.,
chromium-1208) - For Java Playwright and direct Node.js Playwright usage - MCP Playwright Chromium (e.g.,
chromium-1209) - For Claude's browser automation tools (@playwright/mcp) - FFmpeg (for video recording)
This dual installation ensures that Claude's MCP browser tools work without downloading browsers at runtime.
Browsers are installed at /opt/playwright-browsers and owned by the node user (writable for lock files).
To use the pre-installed MCP browsers (avoiding downloads at runtime), pin the @playwright/mcp version in your .mcp.json:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": [
"@playwright/mcp@0.0.64",
"--headless",
"--browser",
"chromium"
]
}
}
}Key options:
@playwright/mcp@X.X.X- Pin to the installed version (check withplaywright-info)--headless- Run without visible browser UI (recommended for containers)--browser chromium- Explicitly use Chromium
Check the installed MCP version:
playwright-info
# Shows: MCP Package: @playwright/mcp@0.0.64Using @latest instead of a pinned version will download new browsers at runtime, which may be slow or fail if the firewall blocks downloads.
The container is configured to work with Java Playwright out of the box:
PLAYWRIGHT_BROWSERS_PATHpoints to pre-installed browsersPLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1prevents unnecessary download attempts- Required GUI libraries (
libgtk-3,libXcursor) are pre-installed
Important: Match your Java Playwright version to the container's Playwright version to use pre-installed browsers.
Check compatibility info:
# Inside the container, run:
playwright-info
# Or check the VERSION file directly:
cat /opt/playwright-browsers/VERSIONExample output:
Standard Playwright: 1.58.1
Chromium: chromium-1208
MCP Package: @playwright/mcp@0.0.64
Playwright: 1.59.0-alpha
Chromium: chromium-1209
Use this version in your Maven pom.xml:
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.58.1</version> <!-- Match PLAYWRIGHT_VERSION -->
</dependency>The container includes cdp-proxy-monitor, a background script that lets you connect Chrome DevTools to Playwright browsers running inside the container.
Playwright MCP launches Chrome with a random --remote-debugging-port each time. The container exposes port 9222 for external Chrome DevTools connections, but there's no way to force Playwright MCP to use a fixed port.
cdp-proxy-monitor runs in the background and:
- Polls every second for a running Chrome process with
--remote-debugging-port - Extracts the actual port Chrome is listening on
- Sets up a socat proxy:
0.0.0.0:9222 -> 127.0.0.1:<chrome-port> - Detects when Chrome dies and waits for a new instance
- Auto-reconnects when Chrome restarts on a different port
| Event | Monitor action |
|---|---|
| Chrome not running | Idles, checks every 1s |
| Chrome starts (port N) | Starts proxy: 9222 -> N, verifies |
| Chrome still on port N | No action |
| Chrome dies | Kills proxy, waits |
| Chrome restarts (port M) | Re-proxies: 9222 -> M, verifies |
-
Start the container with port 9222 exposed:
docker run -it --rm \ --cap-add=NET_ADMIN \ --cap-add=NET_RAW \ -e SKIP_FIREWALL=1 \ -p 9222:9222 \ claude-container:base
-
Inside the container, start the CDP proxy monitor:
nohup cdp-proxy-monitor > /tmp/cdp-proxy.log 2>&1 &
-
Connect from Chrome:
- Forward port 9222 if the container is on a remote host:
ssh -L 9222:localhost:9222 user@docker-host - Open
chrome://inspectin your local Chrome - Click "Configure..." and add
localhost:9222 - Playwright-controlled browsers will appear as remote targets
- Forward port 9222 if the container is on a remote host:
Check the proxy log:
tail -f /tmp/cdp-proxy.logRemote debugging exposes browser internals. Only enable when needed and do not expose port 9222 to untrusted networks.
Symptom: dial tcp 172.64.66.1:443: i/o timeout when pulling Docker images
Solution: Use the claude-docker-host variant instead, or disable the firewall:
# Recommended: Use host socket variant
docker compose up -d claude-docker-host
# Alternative: Disable firewall in DinD
docker run -it --rm --privileged -e SKIP_FIREWALL=1 claude-container:dindSymptom: "Error: Docker daemon failed to start"
Check logs:
# Inside container
cat /var/log/docker.log
# Common issues:
# - Missing privileged mode: Add --privileged flag
# - Missing SYS_ADMIN capability: Add --cap-add=SYS_ADMINSolution: Ensure proper flags:
docker run --rm --privileged \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
--cap-add=SYS_ADMIN \
claude-container:dindSymptom: Connection timeouts or "Could not resolve" warnings
Check firewall status:
# Inside container
firewall-status
# Check if domain is allowed
grep "mydomain.com" /usr/local/etc/allowed-domains.confSolution: Add domain to whitelist:
# Edit allowed-domains.conf
echo "mydomain.com" >> .devcontainer/allowed-domains.conf
# Rebuild image
docker build -t claude-container:base --target base .devcontainer/
# Or reload firewall at runtime (temporary)
# Inside container:
echo "mydomain.com" | sudo tee -a /usr/local/etc/allowed-domains.conf
firewall-reloadSymptom: "Permission denied" when accessing Docker socket
For host socket variant:
# Ensure your user is in docker group on host
sudo usermod -aG docker $USER
newgrp docker
# Or run container with your user's UID
docker run --rm -u $(id -u):$(getent group docker | cut -d: -f3) \
-v /var/run/docker.sock:/var/run/docker.sock \
claude-container:dindSymptom: "invalid argument" when running nested containers
Solution: Use a named volume for /var/lib/docker:
docker run --rm --privileged \
-v claude-docker-data:/var/lib/docker \
claude-container:dindAfter modifying the Dockerfile:
# Docker Compose (base variant)
docker compose build --no-cache claude
# Docker Compose (DinD variants)
docker compose build --no-cache claude-dind
docker compose build --no-cache claude-docker-host
# Docker directly
docker build --no-cache -t claude-container:base --target base .devcontainer/
docker build --no-cache -t claude-container:dind --target dind .devcontainer/For Docker remote setups where the Docker daemon runs on a separate host, use absolute paths on the Docker host:
docker run -it --rm \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
-v "/home/deb/claude-container-config:/claude-container-config" \
-w /workspace \
-e CLAUDE_CONFIG_DIR="/claude-container-config" \
claude-container