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 claudeOn the first start you'll be prompted to authenticate (see Authentication).
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
├── container/
│ ├── claude-config.json # Claude Code config: MCP servers, onboarding, workspace trust
│ ├── claude-container-instructions.md # Instructions for Claude Code inside the container
│ └── 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:
podman-compose --profile build build base
podman-compose build claudeRun claude login inside the container (OAuth), or set ANTHROPIC_API_KEY in .env.
Rebuild without cache:
podman-compose --profile build build base --no-cacheThe Podman socket is started automatically by the entrypoint. Verify with:
podman infoMIT