Run Claude Code in an isolated Docker container. The base image includes Claude Code, MCP servers, Podman, and common dev tools. Extend it with a single FROM to add your project's build tools.
┌─────────────────────────────────────┐
│ claude-code-base │
│ Claude Code CLI, Node 22, Podman, │
│ Playwright, MCP servers, uv/uvx, │
│ Python 3, git-delta, fzf, ripgrep │
└──────────────┬──────────────────────┘
│ FROM claude-code-base
┌──────────────▼──────────────────────┐
│ Your project image │
│ Java, Gradle, .NET, Go, Rust, … │
│ whatever your project needs │
└─────────────────────────────────────┘
| Component | Details |
|---|---|
| Claude Code CLI | Latest (native installer) |
| Node.js | 22 (Debian Bookworm) |
| Podman | Rootless, for Testcontainers / infra |
| Playwright/Chromium | Headless browser automation |
| Python 3 / uv / uvx | For MCP servers and scripting |
| git, jq, ripgrep | Common dev utilities |
| git-delta, fzf | Enhanced CLI experience |
| Server | Purpose |
|---|---|
| context7 | Library/framework documentation lookup |
| serena | Semantic code analysis |
| sequential-thinking | Structured reasoning |
| playwright | Headless browser automation |
- Docker or Podman (with Compose)
- A Claude account — either a Pro/Team subscription (OAuth) or an Anthropic API key
# 1. Build the base image (once, or after updating)
podman-compose --profile build build base
# or: docker compose --profile build build base
# 2. Build the project image
podman-compose build claude
# or: docker compose build claude
# 3. Run
podman-compose run --rm claude
# or: docker compose run --rm claudeOr rebuild both images in one step with the helper script:
./rebuild.sh # rebuild base + project image
./rebuild.sh --no-cache # clean rebuild, ignoring the layer cache
./rebuild.sh --base # rebuild only the base image
./rebuild.sh --project # rebuild only the project imageThe base image's Node version defaults to 22. Override it per team/project by
setting NODE_VERSION when building the base:
NODE_VERSION=20 podman-compose --profile build build base
# or directly: podman build --build-arg NODE_VERSION=20 -f Dockerfile.base .On the first start you'll be prompted to authenticate (see Authentication).
After a build, smoke-test that every tool the Dockerfiles install is present and at the expected version:
./test.sh # test the project image (Java, Maven, .NET, …)
./test.sh base # test the base image onlyThe script pipes container/checks.sh into the container, which prints a ✓ / ! / ✗ report with a pass/warn/fail summary (exit code non-zero on failure). Expected versions live at the top of container/checks.sh — keep them in sync when bumping a pin in a Dockerfile.
The same checks.sh is baked into the image as the test-claude-docker skill, so Claude Code running inside the container can self-check its environment — just ask it to "test the container" or invoke the skill.
Create a Dockerfile in your project that starts from the base:
FROM claude-code-base
# Example: Java project
RUN curl -s "https://get.sdkman.io" | bash \
&& bash -c "source ~/.sdkman/bin/sdkman-init.sh \
&& sdk install java 21.0.10-amzn \
&& sdk install gradle 9.4.1"
ENV SDKMAN_DIR=/home/claude/.sdkman
ENV PATH="${SDKMAN_DIR}/candidates/java/current/bin:${SDKMAN_DIR}/candidates/gradle/current/bin:${PATH}"FROM claude-code-base
# Example: Python project
RUN pip install poetryFROM claude-code-base
# Example: Go project
RUN sudo apt-get update && sudo apt-get install -y golang-go \
&& sudo rm -rf /var/lib/apt/lists/*The included Dockerfile is a working example for a Java/.NET project — use it as a starting point or replace it entirely.
Link the wrapper script to a directory in your PATH:
ln -s /path/to/claude-docker/claude-docker.sh ~/bin/claude-dockerThen use it like the native claude command — the current directory is automatically mounted as the workspace:
cd ~/my-project
claude-dockerTwo options — choose one:
Just start the container. If not yet logged in, it will prompt you with a URL to open in your browser. The login is persisted in the claude-data volume — you only need to do this once.
Add your key to .env:
cp .env.example .env
# Edit .env and set ANTHROPIC_API_KEYOr pass it directly:
ANTHROPIC_API_KEY=sk-ant-xxx claude-dockerclaude-dockerclaude-docker -p "explain this project"
claude-docker -p "find bugs" --output-format jsonclaude-docker bashclaude-docker java --version
claude-docker gradle buildArguments starting with - are passed as flags to Claude Code. Other arguments are executed as standalone commands.
The base image includes a rootless Podman daemon for running containers inside the container. Testcontainers and podman-compose work out of the box.
DOCKER_HOSTis pre-configured to point to the Podman socketTESTCONTAINERS_RYUK_DISABLED=trueis set (Ryuk is incompatible with rootless Podman)- The container runs with
privileged: true— on macOS this is safe because Podman runs inside a Linux VM
No host Docker socket is mounted. All nested containers are fully isolated.
When using the claude-docker wrapper script, Claude Code's project memory is shared between the container and your native Claude Code installation. Memories saved in the container are visible on the host and vice versa.
When using docker compose directly (without the wrapper), memory is stored in a separate Docker volume.
claude-docker/
├── Dockerfile.base # Base image: Node, Podman, Claude Code, MCP servers, Playwright, uv
├── Dockerfile # Project image example: Java, Gradle, Maven, .NET (extend from base)
├── docker-compose.yml # Service definitions with volume mounts
├── claude-docker.sh # Wrapper script for CLI usage
├── rebuild.sh # Helper script to rebuild the base + project images
├── test.sh # Smoke-test: verify installed tools + versions in the image
├── container/
│ ├── claude-config.json # Claude Code config: MCP servers, onboarding, workspace trust
│ ├── claude-container-instructions.md # Instructions for Claude Code inside the container
│ ├── statusline-command.sh # statusLine script (wired into settings.json at startup)
│ ├── checks.sh # Environment checks (run by test.sh and the in-container skill)
│ ├── skills/ # Skills baked into the image (e.g. test-claude-docker)
│ └── entrypoint.sh # Podman socket, auth check, settings init, argument routing
├── CLAUDE.md # Instructions for developing this repo
├── .env.example # Template for environment variables
└── README.md
The container user claude is created with the same UID as your host user (default: 501, macOS default). This ensures mounted workspace files are read/writable without permission issues.
If your UID differs:
HOST_UID=$(id -u) podman-compose --profile build build base
HOST_UID=$(id -u) podman-compose build claudeThe claude-data Docker volume persists ~/.claude between container runs (OAuth credentials, conversation history, settings).
To reset everything:
podman-compose down -vEdit container/claude-config.json and rebuild:
./rebuild.shRun claude login inside the container (OAuth), or set ANTHROPIC_API_KEY in .env.
Rebuild without cache:
./rebuild.sh --no-cacheThe Podman socket is started automatically by the entrypoint. Verify with:
podman infoMIT