Docker Container for Claude Code, including project setup/updates
Find a file
docker-claude 4edef5ac1a
All checks were successful
Build images / check-docker (push) Successful in 2s
Build images / build-and-push (push) Successful in 5m27s
fix stuff finally?
2026-04-15 19:18:48 +02:00
.forgejo/workflows ci: also tag builds as latest 2026-04-15 17:06:53 +02:00
claude use new native install 2026-04-15 19:18:39 +02:00
hooks fix stuff finally? 2026-04-15 19:18:48 +02:00
proxy feat(proxy): allow platform.claude.com in egress allowlist 2026-04-15 17:53:26 +02:00
.dockerignore initial 2026-04-14 20:11:24 +02:00
.env.example refactor(images): pull from registry instead of building; add build.sh for local dev 2026-04-15 17:02:43 +02:00
.gitignore initial 2026-04-14 20:11:24 +02:00
build.sh fix stuff finally? 2026-04-15 19:18:48 +02:00
CLAUDE.md chore(hooks): enforce executable bit on claude.sh and build.sh via pre-commit hook 2026-04-15 17:16:58 +02:00
claude.sh fix stuff finally? 2026-04-15 19:18:48 +02:00
docker-compose.yml fix(workflow): remove build contexts from compose; build.sh uses docker build directly 2026-04-15 17:15:51 +02:00
README.md refactor(images): pull from registry instead of building; add build.sh for local dev 2026-04-15 17:02:43 +02:00

docker-claude

Runs Claude Code inside an isolated Docker environment with a proxy sidecar for controlled egress. Claude cannot reach the host filesystem or network directly.

Architecture

┌──────────────────────────────────────────────────────────┐
│  Host machine                                            │
│                                                          │
│  claude.sh (control script)                              │
│       │                                                  │
│       ▼                                                  │
│  ┌──────────────────────────────────────────────────┐   │
│  │  Docker: claude-secure                           │   │
│  │                                                  │   │
│  │  ┌─────────────┐                                 │   │
│  │  │  claude     │──┐  claude-internal             │   │
│  │  │  (UID 1000) │  │  (internal: true)            │   │
│  │  └─────────────┘  ├──────────────► ┌──────────┐ │   │
│  │  ┌─────────────┐  │                │  proxy   │ │   │
│  │  │  webui      │──┘                │ (UID 13) │ │   │
│  │  │  (UID 1000) │                   └────┬─────┘ │   │
│  │  │  port 7681  │              proxy-external     │   │
│  │  └─────────────┘                        │        │   │
│  └──────────────────────────────────────────────────┘   │
│                                            │             │
│                                            ▼             │
│                                 internet (allowlisted)   │
└──────────────────────────────────────────────────────────┘
  • claude — Claude Code CLI (node:20-alpine), runs as the built-in node user (UID 1000), on claude-internal only
  • webui — Claude Code in a browser terminal via ttyd (node:20-alpine), node user (UID 1000), on claude-internal only, port 7681
  • proxy — Squid forward proxy (alpine:3.21), bridges claude-internal ↔ internet with egress allowlist
  • claude-internalinternal: true; no default gateway, containers cannot reach the internet directly
  • proxy-external — Standard bridge; proxy sidecar only

Prerequisites

  • Docker Engine 24+
  • Docker Compose v2 plugin (docker compose version)

Setup

# 1. Clone / copy this repo
git clone <repo> docker-claude && cd docker-claude

# 2. Configure credentials (see Authentication below)
cp .env.example .env
$EDITOR .env

# 3. Make the control script executable
chmod +x claude.sh

Authentication

Three options — pick one and set it in .env:

Option 1 — API key

ANTHROPIC_API_KEY=sk-ant-...

Option 2 — OAuth token (subscription, headless-friendly)

Run this on your host (not inside the container) to generate a 1-year token:

claude setup-token

Then set the printed token in .env:

CLAUDE_CODE_OAUTH_TOKEN=...

Option 3 — Browser OAuth (interactive)

Leave both keys unset. On first run, Claude Code will print a login URL. Port 54545 must be reachable from your browser for the OAuth callback:

sbx ports <sandbox-name> --publish 54545:54545/tcp

Then run ./claude.sh run and follow the prompt. Credentials are stored in the claude-config Docker volume and reused on every subsequent run.

Usage

CLI mode

# Start proxy, launch Claude Code in the current directory
# (pulls images from registry.zeidler.dev on first run)
cd ~/myproject
./claude.sh start

# Start proxy if needed, launch Claude Code
./claude.sh run

Web interface

Serves Claude Code as a browser terminal via ttyd, protected by HTTP basic auth.

# Add to .env first:
# WEBUI_PASSWORD=your-strong-password
# WEBUI_USER=claude   # optional, defaults to "claude"

./claude.sh web
# → Web interface running at http://0.0.0.0:7681

# To reach it from outside the sandbox host:
sbx ports <sandbox-name> --publish 7681:7681/tcp

# Stop web interface (keeps proxy running)
./claude.sh web-stop

Other commands

./claude.sh stop          # Stop and remove all containers
./claude.sh update        # Pull latest images from the registry
./claude.sh logs          # Tail proxy logs
./claude.sh logs webui    # Tail web interface logs
./claude.sh status        # Show container status
./claude.sh shell         # Debug bash shell in the Claude container

Building locally

build.sh builds both images from source using the local Dockerfiles:

./build.sh              # build with layer cache
./build.sh --no-cache   # force full rebuild

Workspace

Mode Workspace
CLI (run/start) Current working directory (mounted as /workspace)
Web (web) Named Docker volume (claude-web-workspace)

Egress allowlist

Edit proxy/squid.conf and add domains to the allowed_sites ACL:

acl allowed_sites dstdomain api.anthropic.com
acl allowed_sites dstdomain statsig.anthropic.com
# acl allowed_sites dstdomain api.github.com
# acl allowed_sites dstdomain registry.npmjs.org

Rebuild after changes:

./claude.sh update
./claude.sh stop && ./claude.sh start

Security controls

Control claude / webui proxy
Non-root user UID 1000 (node, built into base image) squid user
no-new-privileges yes yes
All capabilities dropped yes yes
Direct internet access no (internal network only) allowlisted only
Host filesystem CWD mounted as /workspace (CLI only) none
Docker socket not mounted not mounted
Web auth basic auth (ttyd --credential) n/a