From e0e5e03e58f6dc88c9ec2772def235f4264970f6 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Tue, 14 Apr 2026 17:23:02 +0200 Subject: [PATCH 01/69] feat(docker): add isolated Claude Code environment with proxy sidecar Two-container setup: claude (UID 1000, internal-only network) and proxy (Squid, UID 13). The internal Docker network uses internal: true so the claude container has no direct internet route. All egress is tunnelled through the Squid sidecar which enforces a domain allowlist. Both containers drop all capabilities and set no-new-privileges. claude.sh provides start/stop/run/update/logs/status/shell lifecycle management. --- .dockerignore | 8 +++ .env.example | 9 +++ .gitignore | 2 + CLAUDE.md | 94 ++++++++++++++++++++++++++ Dockerfile.claude | 30 +++++++++ Dockerfile.proxy | 25 +++++++ README.md | 120 +++++++++++++++++++++++++++++++++ claude.sh | 163 +++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 59 ++++++++++++++++ proxy/squid.conf | 44 ++++++++++++ 10 files changed, 554 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 Dockerfile.claude create mode 100644 Dockerfile.proxy create mode 100644 README.md create mode 100755 claude.sh create mode 100644 docker-compose.yml create mode 100644 proxy/squid.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ba76e9d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.env +*.log +.git +README.md +claude.sh +.gitignore +.env.example +.dockerignore diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d8a6d6f --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Copy this file to .env and fill in your values. +# .env is git-ignored — never commit it. + +# Required: your Anthropic API key +ANTHROPIC_API_KEY=sk-ant-... + +# Optional: mount a host directory as /workspace inside the Claude container. +# If unset, a named Docker volume is used (fully isolated from the host). +# WORKSPACE_DIR=/absolute/path/to/your/project diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2334d82 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +*.log diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9af5a32 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,94 @@ +# Project Guidance + +This file provides context and guidance for working with this project. + +## Project Overview + +**docker-claude** runs Claude Code inside a hardened Docker environment with a Squid proxy sidecar. The goal is full host encapsulation: Claude cannot access the host filesystem or network. All egress is routed through an allowlist-enforcing proxy. + +## Architecture + +Two containers managed by Docker Compose: + +- **`claude`** — Claude Code CLI, non-root (UID 1000), isolated to an internal-only Docker network +- **`proxy`** — Squid forward proxy, non-root (UID 13), bridges the internal network to the internet with an egress allowlist + +Key Docker network property: `claude-internal` has `internal: true`, meaning Docker adds no default gateway. The `claude` container physically cannot reach the internet without going through the `proxy` container. + +## File Structure + +``` +docker-claude/ +├── claude.sh # Control script: start / stop / run / update / logs / status / shell +├── docker-compose.yml # Service definitions and network topology +├── Dockerfile.claude # Claude Code container (node:20-slim, UID 1000) +├── Dockerfile.proxy # Squid proxy sidecar (ubuntu:22.04, UID 13) +├── proxy/ +│ └── squid.conf # Squid ACL config — egress allowlist lives here +├── .env.example # Template for ANTHROPIC_API_KEY +├── .gitignore # Excludes .env and logs +├── .dockerignore # Keeps .env out of build context +└── README.md # User documentation +``` + +## Development Workflow + +```bash +chmod +x claude.sh +cp .env.example .env # set ANTHROPIC_API_KEY +./claude.sh start # build + start proxy + launch Claude interactively +./claude.sh update # rebuild images (no cache) after upstream updates +``` + +## Coding Standards + +- Shell scripts use `set -euo pipefail` +- Dockerfiles use `--no-install-recommends` and clean apt caches in the same layer +- No capabilities granted; `no-new-privileges` on all containers +- `.env` is never committed (enforced by `.gitignore` and `.dockerignore`) +- Commit messages follow **Angular format**: `type(scope): summary` + +## Extending the Egress Allowlist + +Add domains to `proxy/squid.conf` under the `allowed_sites` ACL, then rebuild: + +```bash +docker compose -p claude-secure build proxy +./claude.sh stop && ./claude.sh start +``` + +## Environment Persistence + +This sandbox has a persistent environment file at `/etc/sandbox-persistent.sh`. + +This file is automatically sourced in all shell contexts: +- **Non-interactive shells**: via `BASH_ENV=/etc/sandbox-persistent.sh` +- **Login shells**: via `/etc/profile.d/sandbox-persistent.sh` +- **Interactive shells**: via `/etc/bash.bashrc` and `~/.bashrc` + +## Critical: Shell Completions Must NOT Be in the Persistent Environment File + +**NEVER add shell completion scripts to `/etc/sandbox-persistent.sh`.** + +Shell completion scripts will completely break the bash tool when sourced via the persistent environment file. + +## Network access + +There is a firewall in place to restrict outbound network access. If you need http/https access to +an external service, request it by running: + +```bash +sbx policy allow network [domain] +``` + +## Git Authentication + +The sandbox proxy handles GitHub authentication automatically by injecting credentials for HTTPS Git operations. + +## Additional Notes + +- Always read relevant files before making changes +- Run tests after making modifications +- Follow the existing code structure and patterns +- You have sudo permissions, so you can install necessary packages +- npm, pip and uv are already available for package management diff --git a/Dockerfile.claude b/Dockerfile.claude new file mode 100644 index 0000000..ca847d5 --- /dev/null +++ b/Dockerfile.claude @@ -0,0 +1,30 @@ +FROM node:20-slim + +# Install minimal runtime dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + curl \ + ca-certificates \ + bash \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root user +RUN groupadd -g 1000 claude \ + && useradd -u 1000 -g claude -m -s /bin/bash claude + +# Install Claude Code globally (runs as root for npm -g, then drops) +RUN npm install -g @anthropic-ai/claude-code + +# Workspace directory owned by claude user +RUN mkdir -p /workspace && chown claude:claude /workspace + +USER claude +WORKDIR /workspace + +# Proxy traffic through sidecar — override at runtime if needed +ENV HTTP_PROXY=http://proxy:3128 +ENV HTTPS_PROXY=http://proxy:3128 +ENV ALL_PROXY=http://proxy:3128 +ENV NO_PROXY=localhost,127.0.0.1 + +ENTRYPOINT ["claude"] diff --git a/Dockerfile.proxy b/Dockerfile.proxy new file mode 100644 index 0000000..cfec906 --- /dev/null +++ b/Dockerfile.proxy @@ -0,0 +1,25 @@ +FROM ubuntu:22.04 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + squid \ + && rm -rf /var/lib/apt/lists/* + +# Give the proxy system user (UID 13) ownership of all Squid paths +RUN mkdir -p /var/spool/squid /var/log/squid \ + && chown -R proxy:proxy /var/spool/squid /var/log/squid /etc/squid + +COPY --chown=proxy:proxy proxy/squid.conf /etc/squid/squid.conf + +USER proxy + +# Initialise cache directories as the proxy user +RUN squid -N -f /etc/squid/squid.conf -z 2>/dev/null || true + +EXPOSE 3128 + +HEALTHCHECK --interval=10s --timeout=5s --retries=3 \ + CMD /bin/bash -c 'echo >/dev/tcp/127.0.0.1/3128' + +CMD ["squid", "-N", "-f", "/etc/squid/squid.conf"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..dde60b1 --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +# docker-claude + +Runs [Claude Code](https://claude.ai/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-internal │ │ +│ │ │ claude │◄─────(internal only)───► │ │ +│ │ │ (UID 1000) │ │ │ │ +│ │ └─────────────┘ ┌──────┴──────┐ │ │ +│ │ │ proxy │ │ │ +│ │ │ (UID 13) │ │ │ +│ │ └──────┬──────┘ │ │ +│ │ proxy-external │ │ +│ └─────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ internet (allowlisted) │ +└─────────────────────────────────────────────────────┘ +``` + +- **`claude` container** — Claude Code, runs as UID 1000, on `claude-internal` only (no internet route) +- **`proxy` container** — Squid forward proxy, runs as UID 13, bridges `claude-internal` ↔ internet, enforces egress allowlist +- **`claude-internal`** — Docker bridge with `internal: true`; Docker adds no default gateway, so containers on this network cannot reach the internet directly +- **`proxy-external`** — Standard bridge; the proxy sidecar uses this for controlled outbound access + +## Prerequisites + +- Docker Engine 24+ +- Docker Compose v2 plugin (`docker compose version`) + +## Setup + +```bash +# 1. Clone / copy this repo +git clone docker-claude && cd docker-claude + +# 2. Configure your API key +cp .env.example .env +$EDITOR .env # set ANTHROPIC_API_KEY + +# 3. Make the control script executable +chmod +x claude.sh +``` + +## Usage + +```bash +# Build images, start proxy, launch Claude Code interactively +./claude.sh start + +# Same as start but skips image rebuild (faster on subsequent runs) +./claude.sh run + +# Stop and remove all containers (proxy + any running sessions) +./claude.sh stop + +# Rebuild images without cache (e.g. after Claude Code updates) +./claude.sh update + +# Tail proxy access logs +./claude.sh logs + +# Show container status +./claude.sh status + +# Open a debug bash shell inside the Claude container +./claude.sh shell +``` + +### Working with host files + +By default, Claude's workspace is a named Docker volume (`claude-secure-workspace`) — fully isolated from the host. + +To mount a specific host directory: + +```bash +WORKSPACE_DIR=$HOME/myproject ./claude.sh run +``` + +The directory is mounted at `/workspace` inside the container. + +## Egress allowlist + +Edit `proxy/squid.conf` and add domains to the `allowed_sites` ACL: + +```squid +acl allowed_sites dstdomain api.anthropic.com +acl allowed_sites dstdomain statsig.anthropic.com +# acl allowed_sites dstdomain api.github.com # uncomment if needed +# acl allowed_sites dstdomain registry.npmjs.org +``` + +Rebuild the proxy after changes: + +```bash +docker compose -p claude-secure build proxy +./claude.sh stop && ./claude.sh start +``` + +## Security controls + +| Control | Claude container | Proxy container | +|---|---|---| +| Non-root user | UID 1000 (`claude`) | UID 13 (`proxy`) | +| `no-new-privileges` | yes | yes | +| All capabilities dropped | yes | yes | +| Direct internet access | no (`internal` network only) | allowlisted only | +| Host filesystem | no mounts by default | none | +| Docker socket | not mounted | not mounted | diff --git a/claude.sh b/claude.sh new file mode 100755 index 0000000..52bcc3a --- /dev/null +++ b/claude.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +# claude.sh — Manage the isolated Claude Code Docker environment +# Usage: ./claude.sh [args] +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml" +PROJECT="claude-secure" + +# ─── Colours ────────────────────────────────────────────────────────────────── +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' +info() { echo -e "${GREEN}[+]${NC} $*"; } +warn() { echo -e "${YELLOW}[!]${NC} $*"; } +error() { echo -e "${RED}[-]${NC} $*" >&2; } + +# ─── Dependency check ───────────────────────────────────────────────────────── +check_deps() { + if ! command -v docker &>/dev/null; then + error "Docker is not installed. https://docs.docker.com/get-docker/" + exit 1 + fi + if ! docker compose version &>/dev/null 2>&1; then + error "Docker Compose v2 plugin is required." + exit 1 + fi +} + +# ─── Environment loading ────────────────────────────────────────────────────── +load_env() { + local env_file="$SCRIPT_DIR/.env" + if [[ -f "$env_file" ]]; then + # shellcheck disable=SC1090 + set -a; source "$env_file"; set +a + fi + if [[ -z "${ANTHROPIC_API_KEY:-}" ]]; then + error "ANTHROPIC_API_KEY is not set." + error "Copy .env.example → .env and add your key, or export it in your shell." + exit 1 + fi +} + +# ─── Workspace volume resolution ────────────────────────────────────────────── +# Default: named Docker volume (fully isolated). +# Override: export WORKSPACE_DIR=/path/to/project before running. +workspace_flag() { + if [[ -n "${WORKSPACE_DIR:-}" ]]; then + local abs + abs="$(realpath "${WORKSPACE_DIR}")" + if [[ ! -d "$abs" ]]; then + error "WORKSPACE_DIR does not exist: $abs" + exit 1 + fi + echo "--volume ${abs}:/workspace:z" + else + echo "--volume ${PROJECT}-workspace:/workspace" + fi +} + +# ─── Compose wrapper ────────────────────────────────────────────────────────── +dc() { docker compose -f "$COMPOSE_FILE" -p "$PROJECT" "$@"; } + +# ─── Commands ───────────────────────────────────────────────────────────────── + +cmd_start() { + check_deps + load_env + info "Building images..." + dc build + info "Starting proxy sidecar..." + dc up -d proxy + info "Waiting for proxy health check..." + dc up -d proxy # no-op if already healthy; compose waits via depends_on + info "Launching Claude Code..." + # shellcheck disable=SC2046 + dc run --rm $(workspace_flag) claude "$@" +} + +cmd_stop() { + check_deps + info "Stopping all containers..." + dc down + info "Done." +} + +cmd_run() { + check_deps + load_env + info "Ensuring proxy is running..." + dc up -d proxy + info "Launching Claude Code..." + # shellcheck disable=SC2046 + dc run --rm $(workspace_flag) claude "$@" +} + +cmd_update() { + check_deps + info "Rebuilding images (no cache)..." + dc build --no-cache + info "Update complete. Run './claude.sh start' to launch." +} + +cmd_logs() { + check_deps + local svc="${1:-proxy}" + dc logs -f "$svc" +} + +cmd_status() { + check_deps + dc ps +} + +cmd_shell() { + check_deps + load_env + warn "Opening debug shell inside Claude container (non-Claude entrypoint)." + # shellcheck disable=SC2046 + dc run --rm --entrypoint /bin/bash $(workspace_flag) claude +} + +cmd_help() { + cat < [args] + +Commands: + start [args] Build images, start proxy, launch Claude Code + run [args] Start proxy if needed, launch Claude Code + stop Stop and remove all containers + update Rebuild images without cache + logs [svc] Tail logs (default: proxy) + status Show container status + shell Open a bash shell in the Claude container (debug) + help Show this message + +Environment variables: + ANTHROPIC_API_KEY Required. Set in .env or exported in your shell. + WORKSPACE_DIR Optional. Absolute path to mount as /workspace. + Defaults to a named Docker volume (fully isolated). + +Examples: + ./claude.sh start + WORKSPACE_DIR=\$HOME/myproject ./claude.sh run + ./claude.sh logs proxy + ./claude.sh shell +EOF +} + +# ─── Dispatch ───────────────────────────────────────────────────────────────── +case "${1:-help}" in + start) shift; cmd_start "$@" ;; + stop) cmd_stop ;; + run) shift; cmd_run "$@" ;; + update) cmd_update ;; + logs) shift; cmd_logs "${1:-}" ;; + status) cmd_status ;; + shell) cmd_shell ;; + help|-h|--help) cmd_help ;; + *) + error "Unknown command: ${1}" + cmd_help + exit 1 + ;; +esac diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..baadf41 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,59 @@ +services: + + # ─── Proxy sidecar ───────────────────────────────────────────────────────── + # Bridges the isolated internal network to the internet. + # Enforces an egress allowlist — see proxy/squid.conf. + proxy: + build: + context: . + dockerfile: Dockerfile.proxy + networks: + - claude-internal # reachable by the claude container + - proxy-external # has outbound internet access + restart: unless-stopped + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + read_only: true + tmpfs: + - /tmp + - /var/spool/squid + - /var/log/squid + + # ─── Claude Code container ───────────────────────────────────────────────── + # No direct internet access. All egress routes through the proxy sidecar. + # Run via "docker compose run --rm claude" (managed by claude.sh). + claude: + build: + context: . + dockerfile: Dockerfile.claude + depends_on: + proxy: + condition: service_healthy + networks: + - claude-internal # only — no route to the internet + environment: + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + - HTTP_PROXY=http://proxy:3128 + - HTTPS_PROXY=http://proxy:3128 + - ALL_PROXY=http://proxy:3128 + - NO_PROXY=localhost,127.0.0.1 + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + stdin_open: true + tty: true + # Workspace is injected by claude.sh via --volume flag at run time. + # Default: named Docker volume. Override: set WORKSPACE_DIR on the host. + +networks: + # Internal-only: Docker adds no default gateway → no direct internet route + claude-internal: + driver: bridge + internal: true + + # External: standard bridge with internet access (proxy only) + proxy-external: + driver: bridge diff --git a/proxy/squid.conf b/proxy/squid.conf new file mode 100644 index 0000000..6ef039f --- /dev/null +++ b/proxy/squid.conf @@ -0,0 +1,44 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Squid forward-proxy sidecar — allowlist-only egress for Claude Code +# ───────────────────────────────────────────────────────────────────────────── + +http_port 3128 + +# PID must be writable by the non-root proxy user +pid_filename /tmp/squid.pid + +# ─── Logging (container-friendly: stdout/stderr) ────────────────────────────── +access_log stdio:/dev/stdout combined +cache_log stdio:/dev/stderr +cache_store_log none + +# ─── No disk cache ──────────────────────────────────────────────────────────── +cache deny all +coredump_dir /var/spool/squid + +# ─── ACL Definitions ────────────────────────────────────────────────────────── +acl SSL_ports port 443 +acl Safe_ports port 80 +acl Safe_ports port 443 +acl CONNECT method CONNECT + +# ─── Egress allowlist ───────────────────────────────────────────────────────── +# Add domains here as needed. Leading dot matches all subdomains. +acl allowed_sites dstdomain api.anthropic.com +acl allowed_sites dstdomain statsig.anthropic.com + +# ─── Access rules ───────────────────────────────────────────────────────────── +# Block requests to non-standard ports +http_access deny !Safe_ports + +# Block CONNECT to non-SSL ports +http_access deny CONNECT !SSL_ports + +# Allow HTTPS tunnels only to allowlisted destinations +http_access allow CONNECT allowed_sites + +# Allow plain HTTP only to allowlisted destinations +http_access allow allowed_sites + +# Deny everything else — default deny +http_access deny all From 66c74ee396e399a4b0b17749f8de535b36898f9c Mon Sep 17 00:00:00 2001 From: Julius Zeidler Date: Tue, 14 Apr 2026 17:25:33 +0200 Subject: [PATCH 02/69] initial config --- claude.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 claude.sh diff --git a/claude.sh b/claude.sh old mode 100755 new mode 100644 From 0cba6bc8b9dee7eef1edcd0d4d62b863ef8ec5e5 Mon Sep 17 00:00:00 2001 From: Julius Zeidler Date: Tue, 14 Apr 2026 17:30:39 +0200 Subject: [PATCH 03/69] chore(script): set claude.sh as executable --- claude.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 claude.sh diff --git a/claude.sh b/claude.sh old mode 100644 new mode 100755 From c01102b641fea6d88fe66d3782d5348191cea3eb Mon Sep 17 00:00:00 2001 From: Julius Zeidler Date: Tue, 14 Apr 2026 20:11:24 +0200 Subject: [PATCH 04/69] initial --- .dockerignore | 8 +++ .env.example | 9 +++ .gitignore | 2 + CLAUDE.md | 94 ++++++++++++++++++++++++++ Dockerfile.claude | 30 +++++++++ Dockerfile.proxy | 25 +++++++ README.md | 120 +++++++++++++++++++++++++++++++++ claude.sh | 163 +++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 59 ++++++++++++++++ proxy/squid.conf | 44 ++++++++++++ 10 files changed, 554 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 Dockerfile.claude create mode 100644 Dockerfile.proxy create mode 100644 README.md create mode 100755 claude.sh create mode 100644 docker-compose.yml create mode 100644 proxy/squid.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ba76e9d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.env +*.log +.git +README.md +claude.sh +.gitignore +.env.example +.dockerignore diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d8a6d6f --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Copy this file to .env and fill in your values. +# .env is git-ignored — never commit it. + +# Required: your Anthropic API key +ANTHROPIC_API_KEY=sk-ant-... + +# Optional: mount a host directory as /workspace inside the Claude container. +# If unset, a named Docker volume is used (fully isolated from the host). +# WORKSPACE_DIR=/absolute/path/to/your/project diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2334d82 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +*.log diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9af5a32 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,94 @@ +# Project Guidance + +This file provides context and guidance for working with this project. + +## Project Overview + +**docker-claude** runs Claude Code inside a hardened Docker environment with a Squid proxy sidecar. The goal is full host encapsulation: Claude cannot access the host filesystem or network. All egress is routed through an allowlist-enforcing proxy. + +## Architecture + +Two containers managed by Docker Compose: + +- **`claude`** — Claude Code CLI, non-root (UID 1000), isolated to an internal-only Docker network +- **`proxy`** — Squid forward proxy, non-root (UID 13), bridges the internal network to the internet with an egress allowlist + +Key Docker network property: `claude-internal` has `internal: true`, meaning Docker adds no default gateway. The `claude` container physically cannot reach the internet without going through the `proxy` container. + +## File Structure + +``` +docker-claude/ +├── claude.sh # Control script: start / stop / run / update / logs / status / shell +├── docker-compose.yml # Service definitions and network topology +├── Dockerfile.claude # Claude Code container (node:20-slim, UID 1000) +├── Dockerfile.proxy # Squid proxy sidecar (ubuntu:22.04, UID 13) +├── proxy/ +│ └── squid.conf # Squid ACL config — egress allowlist lives here +├── .env.example # Template for ANTHROPIC_API_KEY +├── .gitignore # Excludes .env and logs +├── .dockerignore # Keeps .env out of build context +└── README.md # User documentation +``` + +## Development Workflow + +```bash +chmod +x claude.sh +cp .env.example .env # set ANTHROPIC_API_KEY +./claude.sh start # build + start proxy + launch Claude interactively +./claude.sh update # rebuild images (no cache) after upstream updates +``` + +## Coding Standards + +- Shell scripts use `set -euo pipefail` +- Dockerfiles use `--no-install-recommends` and clean apt caches in the same layer +- No capabilities granted; `no-new-privileges` on all containers +- `.env` is never committed (enforced by `.gitignore` and `.dockerignore`) +- Commit messages follow **Angular format**: `type(scope): summary` + +## Extending the Egress Allowlist + +Add domains to `proxy/squid.conf` under the `allowed_sites` ACL, then rebuild: + +```bash +docker compose -p claude-secure build proxy +./claude.sh stop && ./claude.sh start +``` + +## Environment Persistence + +This sandbox has a persistent environment file at `/etc/sandbox-persistent.sh`. + +This file is automatically sourced in all shell contexts: +- **Non-interactive shells**: via `BASH_ENV=/etc/sandbox-persistent.sh` +- **Login shells**: via `/etc/profile.d/sandbox-persistent.sh` +- **Interactive shells**: via `/etc/bash.bashrc` and `~/.bashrc` + +## Critical: Shell Completions Must NOT Be in the Persistent Environment File + +**NEVER add shell completion scripts to `/etc/sandbox-persistent.sh`.** + +Shell completion scripts will completely break the bash tool when sourced via the persistent environment file. + +## Network access + +There is a firewall in place to restrict outbound network access. If you need http/https access to +an external service, request it by running: + +```bash +sbx policy allow network [domain] +``` + +## Git Authentication + +The sandbox proxy handles GitHub authentication automatically by injecting credentials for HTTPS Git operations. + +## Additional Notes + +- Always read relevant files before making changes +- Run tests after making modifications +- Follow the existing code structure and patterns +- You have sudo permissions, so you can install necessary packages +- npm, pip and uv are already available for package management diff --git a/Dockerfile.claude b/Dockerfile.claude new file mode 100644 index 0000000..ca847d5 --- /dev/null +++ b/Dockerfile.claude @@ -0,0 +1,30 @@ +FROM node:20-slim + +# Install minimal runtime dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + curl \ + ca-certificates \ + bash \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root user +RUN groupadd -g 1000 claude \ + && useradd -u 1000 -g claude -m -s /bin/bash claude + +# Install Claude Code globally (runs as root for npm -g, then drops) +RUN npm install -g @anthropic-ai/claude-code + +# Workspace directory owned by claude user +RUN mkdir -p /workspace && chown claude:claude /workspace + +USER claude +WORKDIR /workspace + +# Proxy traffic through sidecar — override at runtime if needed +ENV HTTP_PROXY=http://proxy:3128 +ENV HTTPS_PROXY=http://proxy:3128 +ENV ALL_PROXY=http://proxy:3128 +ENV NO_PROXY=localhost,127.0.0.1 + +ENTRYPOINT ["claude"] diff --git a/Dockerfile.proxy b/Dockerfile.proxy new file mode 100644 index 0000000..cfec906 --- /dev/null +++ b/Dockerfile.proxy @@ -0,0 +1,25 @@ +FROM ubuntu:22.04 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + squid \ + && rm -rf /var/lib/apt/lists/* + +# Give the proxy system user (UID 13) ownership of all Squid paths +RUN mkdir -p /var/spool/squid /var/log/squid \ + && chown -R proxy:proxy /var/spool/squid /var/log/squid /etc/squid + +COPY --chown=proxy:proxy proxy/squid.conf /etc/squid/squid.conf + +USER proxy + +# Initialise cache directories as the proxy user +RUN squid -N -f /etc/squid/squid.conf -z 2>/dev/null || true + +EXPOSE 3128 + +HEALTHCHECK --interval=10s --timeout=5s --retries=3 \ + CMD /bin/bash -c 'echo >/dev/tcp/127.0.0.1/3128' + +CMD ["squid", "-N", "-f", "/etc/squid/squid.conf"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..dde60b1 --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +# docker-claude + +Runs [Claude Code](https://claude.ai/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-internal │ │ +│ │ │ claude │◄─────(internal only)───► │ │ +│ │ │ (UID 1000) │ │ │ │ +│ │ └─────────────┘ ┌──────┴──────┐ │ │ +│ │ │ proxy │ │ │ +│ │ │ (UID 13) │ │ │ +│ │ └──────┬──────┘ │ │ +│ │ proxy-external │ │ +│ └─────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ internet (allowlisted) │ +└─────────────────────────────────────────────────────┘ +``` + +- **`claude` container** — Claude Code, runs as UID 1000, on `claude-internal` only (no internet route) +- **`proxy` container** — Squid forward proxy, runs as UID 13, bridges `claude-internal` ↔ internet, enforces egress allowlist +- **`claude-internal`** — Docker bridge with `internal: true`; Docker adds no default gateway, so containers on this network cannot reach the internet directly +- **`proxy-external`** — Standard bridge; the proxy sidecar uses this for controlled outbound access + +## Prerequisites + +- Docker Engine 24+ +- Docker Compose v2 plugin (`docker compose version`) + +## Setup + +```bash +# 1. Clone / copy this repo +git clone docker-claude && cd docker-claude + +# 2. Configure your API key +cp .env.example .env +$EDITOR .env # set ANTHROPIC_API_KEY + +# 3. Make the control script executable +chmod +x claude.sh +``` + +## Usage + +```bash +# Build images, start proxy, launch Claude Code interactively +./claude.sh start + +# Same as start but skips image rebuild (faster on subsequent runs) +./claude.sh run + +# Stop and remove all containers (proxy + any running sessions) +./claude.sh stop + +# Rebuild images without cache (e.g. after Claude Code updates) +./claude.sh update + +# Tail proxy access logs +./claude.sh logs + +# Show container status +./claude.sh status + +# Open a debug bash shell inside the Claude container +./claude.sh shell +``` + +### Working with host files + +By default, Claude's workspace is a named Docker volume (`claude-secure-workspace`) — fully isolated from the host. + +To mount a specific host directory: + +```bash +WORKSPACE_DIR=$HOME/myproject ./claude.sh run +``` + +The directory is mounted at `/workspace` inside the container. + +## Egress allowlist + +Edit `proxy/squid.conf` and add domains to the `allowed_sites` ACL: + +```squid +acl allowed_sites dstdomain api.anthropic.com +acl allowed_sites dstdomain statsig.anthropic.com +# acl allowed_sites dstdomain api.github.com # uncomment if needed +# acl allowed_sites dstdomain registry.npmjs.org +``` + +Rebuild the proxy after changes: + +```bash +docker compose -p claude-secure build proxy +./claude.sh stop && ./claude.sh start +``` + +## Security controls + +| Control | Claude container | Proxy container | +|---|---|---| +| Non-root user | UID 1000 (`claude`) | UID 13 (`proxy`) | +| `no-new-privileges` | yes | yes | +| All capabilities dropped | yes | yes | +| Direct internet access | no (`internal` network only) | allowlisted only | +| Host filesystem | no mounts by default | none | +| Docker socket | not mounted | not mounted | diff --git a/claude.sh b/claude.sh new file mode 100755 index 0000000..52bcc3a --- /dev/null +++ b/claude.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +# claude.sh — Manage the isolated Claude Code Docker environment +# Usage: ./claude.sh [args] +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml" +PROJECT="claude-secure" + +# ─── Colours ────────────────────────────────────────────────────────────────── +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' +info() { echo -e "${GREEN}[+]${NC} $*"; } +warn() { echo -e "${YELLOW}[!]${NC} $*"; } +error() { echo -e "${RED}[-]${NC} $*" >&2; } + +# ─── Dependency check ───────────────────────────────────────────────────────── +check_deps() { + if ! command -v docker &>/dev/null; then + error "Docker is not installed. https://docs.docker.com/get-docker/" + exit 1 + fi + if ! docker compose version &>/dev/null 2>&1; then + error "Docker Compose v2 plugin is required." + exit 1 + fi +} + +# ─── Environment loading ────────────────────────────────────────────────────── +load_env() { + local env_file="$SCRIPT_DIR/.env" + if [[ -f "$env_file" ]]; then + # shellcheck disable=SC1090 + set -a; source "$env_file"; set +a + fi + if [[ -z "${ANTHROPIC_API_KEY:-}" ]]; then + error "ANTHROPIC_API_KEY is not set." + error "Copy .env.example → .env and add your key, or export it in your shell." + exit 1 + fi +} + +# ─── Workspace volume resolution ────────────────────────────────────────────── +# Default: named Docker volume (fully isolated). +# Override: export WORKSPACE_DIR=/path/to/project before running. +workspace_flag() { + if [[ -n "${WORKSPACE_DIR:-}" ]]; then + local abs + abs="$(realpath "${WORKSPACE_DIR}")" + if [[ ! -d "$abs" ]]; then + error "WORKSPACE_DIR does not exist: $abs" + exit 1 + fi + echo "--volume ${abs}:/workspace:z" + else + echo "--volume ${PROJECT}-workspace:/workspace" + fi +} + +# ─── Compose wrapper ────────────────────────────────────────────────────────── +dc() { docker compose -f "$COMPOSE_FILE" -p "$PROJECT" "$@"; } + +# ─── Commands ───────────────────────────────────────────────────────────────── + +cmd_start() { + check_deps + load_env + info "Building images..." + dc build + info "Starting proxy sidecar..." + dc up -d proxy + info "Waiting for proxy health check..." + dc up -d proxy # no-op if already healthy; compose waits via depends_on + info "Launching Claude Code..." + # shellcheck disable=SC2046 + dc run --rm $(workspace_flag) claude "$@" +} + +cmd_stop() { + check_deps + info "Stopping all containers..." + dc down + info "Done." +} + +cmd_run() { + check_deps + load_env + info "Ensuring proxy is running..." + dc up -d proxy + info "Launching Claude Code..." + # shellcheck disable=SC2046 + dc run --rm $(workspace_flag) claude "$@" +} + +cmd_update() { + check_deps + info "Rebuilding images (no cache)..." + dc build --no-cache + info "Update complete. Run './claude.sh start' to launch." +} + +cmd_logs() { + check_deps + local svc="${1:-proxy}" + dc logs -f "$svc" +} + +cmd_status() { + check_deps + dc ps +} + +cmd_shell() { + check_deps + load_env + warn "Opening debug shell inside Claude container (non-Claude entrypoint)." + # shellcheck disable=SC2046 + dc run --rm --entrypoint /bin/bash $(workspace_flag) claude +} + +cmd_help() { + cat < [args] + +Commands: + start [args] Build images, start proxy, launch Claude Code + run [args] Start proxy if needed, launch Claude Code + stop Stop and remove all containers + update Rebuild images without cache + logs [svc] Tail logs (default: proxy) + status Show container status + shell Open a bash shell in the Claude container (debug) + help Show this message + +Environment variables: + ANTHROPIC_API_KEY Required. Set in .env or exported in your shell. + WORKSPACE_DIR Optional. Absolute path to mount as /workspace. + Defaults to a named Docker volume (fully isolated). + +Examples: + ./claude.sh start + WORKSPACE_DIR=\$HOME/myproject ./claude.sh run + ./claude.sh logs proxy + ./claude.sh shell +EOF +} + +# ─── Dispatch ───────────────────────────────────────────────────────────────── +case "${1:-help}" in + start) shift; cmd_start "$@" ;; + stop) cmd_stop ;; + run) shift; cmd_run "$@" ;; + update) cmd_update ;; + logs) shift; cmd_logs "${1:-}" ;; + status) cmd_status ;; + shell) cmd_shell ;; + help|-h|--help) cmd_help ;; + *) + error "Unknown command: ${1}" + cmd_help + exit 1 + ;; +esac diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..baadf41 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,59 @@ +services: + + # ─── Proxy sidecar ───────────────────────────────────────────────────────── + # Bridges the isolated internal network to the internet. + # Enforces an egress allowlist — see proxy/squid.conf. + proxy: + build: + context: . + dockerfile: Dockerfile.proxy + networks: + - claude-internal # reachable by the claude container + - proxy-external # has outbound internet access + restart: unless-stopped + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + read_only: true + tmpfs: + - /tmp + - /var/spool/squid + - /var/log/squid + + # ─── Claude Code container ───────────────────────────────────────────────── + # No direct internet access. All egress routes through the proxy sidecar. + # Run via "docker compose run --rm claude" (managed by claude.sh). + claude: + build: + context: . + dockerfile: Dockerfile.claude + depends_on: + proxy: + condition: service_healthy + networks: + - claude-internal # only — no route to the internet + environment: + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + - HTTP_PROXY=http://proxy:3128 + - HTTPS_PROXY=http://proxy:3128 + - ALL_PROXY=http://proxy:3128 + - NO_PROXY=localhost,127.0.0.1 + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + stdin_open: true + tty: true + # Workspace is injected by claude.sh via --volume flag at run time. + # Default: named Docker volume. Override: set WORKSPACE_DIR on the host. + +networks: + # Internal-only: Docker adds no default gateway → no direct internet route + claude-internal: + driver: bridge + internal: true + + # External: standard bridge with internet access (proxy only) + proxy-external: + driver: bridge diff --git a/proxy/squid.conf b/proxy/squid.conf new file mode 100644 index 0000000..6ef039f --- /dev/null +++ b/proxy/squid.conf @@ -0,0 +1,44 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Squid forward-proxy sidecar — allowlist-only egress for Claude Code +# ───────────────────────────────────────────────────────────────────────────── + +http_port 3128 + +# PID must be writable by the non-root proxy user +pid_filename /tmp/squid.pid + +# ─── Logging (container-friendly: stdout/stderr) ────────────────────────────── +access_log stdio:/dev/stdout combined +cache_log stdio:/dev/stderr +cache_store_log none + +# ─── No disk cache ──────────────────────────────────────────────────────────── +cache deny all +coredump_dir /var/spool/squid + +# ─── ACL Definitions ────────────────────────────────────────────────────────── +acl SSL_ports port 443 +acl Safe_ports port 80 +acl Safe_ports port 443 +acl CONNECT method CONNECT + +# ─── Egress allowlist ───────────────────────────────────────────────────────── +# Add domains here as needed. Leading dot matches all subdomains. +acl allowed_sites dstdomain api.anthropic.com +acl allowed_sites dstdomain statsig.anthropic.com + +# ─── Access rules ───────────────────────────────────────────────────────────── +# Block requests to non-standard ports +http_access deny !Safe_ports + +# Block CONNECT to non-SSL ports +http_access deny CONNECT !SSL_ports + +# Allow HTTPS tunnels only to allowlisted destinations +http_access allow CONNECT allowed_sites + +# Allow plain HTTP only to allowlisted destinations +http_access allow allowed_sites + +# Deny everything else — default deny +http_access deny all From 9b8562b746c6ba17d80a96c679eb4a0bea5c6739 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Tue, 14 Apr 2026 22:25:38 +0200 Subject: [PATCH 05/69] feat(webui): add browser terminal interface via ttyd Adds a webui service to docker-compose that wraps Claude Code in ttyd, serving a browser-accessible terminal on port 7681. The webui reuses Dockerfile.claude (ttyd added to apt deps) with a dedicated entrypoint script that enforces WEBUI_PASSWORD before starting. Network isolation is identical to the CLI container: claude-internal only, all egress via the proxy allowlist. claude.sh gains web and web-stop commands. --- .env.example | 4 ++ CLAUDE.md | 31 +++++----- Dockerfile.claude | 4 ++ README.md | 134 +++++++++++++++++++++++++------------------- claude.sh | 71 ++++++++++++++++------- docker-compose.yml | 43 +++++++++++++- webui-entrypoint.sh | 14 +++++ 7 files changed, 209 insertions(+), 92 deletions(-) create mode 100644 webui-entrypoint.sh diff --git a/.env.example b/.env.example index d8a6d6f..00a8187 100644 --- a/.env.example +++ b/.env.example @@ -7,3 +7,7 @@ ANTHROPIC_API_KEY=sk-ant-... # Optional: mount a host directory as /workspace inside the Claude container. # If unset, a named Docker volume is used (fully isolated from the host). # WORKSPACE_DIR=/absolute/path/to/your/project + +# Web interface credentials (required for ./claude.sh web) +# WEBUI_USER=claude +# WEBUI_PASSWORD=changeme diff --git a/CLAUDE.md b/CLAUDE.md index 9af5a32..6c93531 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,35 +8,40 @@ This file provides context and guidance for working with this project. ## Architecture -Two containers managed by Docker Compose: +Three containers managed by Docker Compose: - **`claude`** — Claude Code CLI, non-root (UID 1000), isolated to an internal-only Docker network +- **`webui`** — Claude Code as a browser terminal (ttyd on port 7681), same image as `claude`, non-root (UID 1000), same network isolation, basic auth required - **`proxy`** — Squid forward proxy, non-root (UID 13), bridges the internal network to the internet with an egress allowlist -Key Docker network property: `claude-internal` has `internal: true`, meaning Docker adds no default gateway. The `claude` container physically cannot reach the internet without going through the `proxy` container. +Key Docker network property: `claude-internal` has `internal: true`, meaning Docker adds no default gateway. The `claude` and `webui` containers physically cannot reach the internet without going through the `proxy` container. + +The `webui` service reuses `Dockerfile.claude`. Its entrypoint (`webui-entrypoint.sh`) starts `ttyd --credential user:pass claude` instead of `claude` directly. ## File Structure ``` docker-claude/ -├── claude.sh # Control script: start / stop / run / update / logs / status / shell -├── docker-compose.yml # Service definitions and network topology -├── Dockerfile.claude # Claude Code container (node:20-slim, UID 1000) -├── Dockerfile.proxy # Squid proxy sidecar (ubuntu:22.04, UID 13) +├── claude.sh # Control script: start/stop/run/web/web-stop/update/logs/status/shell +├── docker-compose.yml # Service definitions and network topology +├── Dockerfile.claude # Claude Code + ttyd container (node:20-slim, UID 1000) +├── Dockerfile.proxy # Squid proxy sidecar (ubuntu:22.04, UID 13) +├── webui-entrypoint.sh # Entrypoint for webui service: starts ttyd wrapping claude ├── proxy/ -│ └── squid.conf # Squid ACL config — egress allowlist lives here -├── .env.example # Template for ANTHROPIC_API_KEY -├── .gitignore # Excludes .env and logs -├── .dockerignore # Keeps .env out of build context -└── README.md # User documentation +│ └── squid.conf # Squid ACL config — egress allowlist lives here +├── .env.example # Template for ANTHROPIC_API_KEY, WEBUI_PASSWORD, etc. +├── .gitignore # Excludes .env and logs +├── .dockerignore # Keeps .env out of build context +└── README.md # User documentation ``` ## Development Workflow ```bash chmod +x claude.sh -cp .env.example .env # set ANTHROPIC_API_KEY -./claude.sh start # build + start proxy + launch Claude interactively +cp .env.example .env # set ANTHROPIC_API_KEY (and WEBUI_PASSWORD for web mode) +./claude.sh start # build + start proxy + launch Claude interactively (CLI) +./claude.sh web # build + start proxy + webui (browser terminal on :7681) ./claude.sh update # rebuild images (no cache) after upstream updates ``` diff --git a/Dockerfile.claude b/Dockerfile.claude index ca847d5..9757f8a 100644 --- a/Dockerfile.claude +++ b/Dockerfile.claude @@ -6,8 +6,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ ca-certificates \ bash \ + ttyd \ && rm -rf /var/lib/apt/lists/* +# Entrypoint used by the webui service (ttyd wrapping claude) +COPY --chmod=755 webui-entrypoint.sh /usr/local/bin/webui-entrypoint.sh + # Create non-root user RUN groupadd -g 1000 claude \ && useradd -u 1000 -g claude -m -s /bin/bash claude diff --git a/README.md b/README.md index dde60b1..1fd96a6 100644 --- a/README.md +++ b/README.md @@ -5,34 +5,36 @@ Runs [Claude Code](https://claude.ai/code) inside an isolated Docker environment ## Architecture ``` -┌─────────────────────────────────────────────────────┐ -│ Host machine │ -│ │ -│ claude.sh (control script) │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────┐ │ -│ │ Docker: claude-secure │ │ -│ │ │ │ -│ │ ┌─────────────┐ claude-internal │ │ -│ │ │ claude │◄─────(internal only)───► │ │ -│ │ │ (UID 1000) │ │ │ │ -│ │ └─────────────┘ ┌──────┴──────┐ │ │ -│ │ │ proxy │ │ │ -│ │ │ (UID 13) │ │ │ -│ │ └──────┬──────┘ │ │ -│ │ proxy-external │ │ -│ └─────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ internet (allowlisted) │ -└─────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────┐ +│ 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` container** — Claude Code, runs as UID 1000, on `claude-internal` only (no internet route) -- **`proxy` container** — Squid forward proxy, runs as UID 13, bridges `claude-internal` ↔ internet, enforces egress allowlist -- **`claude-internal`** — Docker bridge with `internal: true`; Docker adds no default gateway, so containers on this network cannot reach the internet directly -- **`proxy-external`** — Standard bridge; the proxy sidecar uses this for controlled outbound access +- **`claude`** — Claude Code CLI, UID 1000, on `claude-internal` only +- **`webui`** — Claude Code in a browser terminal (ttyd), UID 1000, on `claude-internal` only, port 7681 +- **`proxy`** — Squid forward proxy, UID 13, bridges `claude-internal` ↔ internet with egress allowlist +- **`claude-internal`** — `internal: true`; no default gateway, containers cannot reach the internet directly +- **`proxy-external`** — Standard bridge; proxy sidecar only ## Prerequisites @@ -45,9 +47,9 @@ Runs [Claude Code](https://claude.ai/code) inside an isolated Docker environment # 1. Clone / copy this repo git clone docker-claude && cd docker-claude -# 2. Configure your API key +# 2. Configure credentials cp .env.example .env -$EDITOR .env # set ANTHROPIC_API_KEY +$EDITOR .env # set ANTHROPIC_API_KEY (and WEBUI_PASSWORD if using web mode) # 3. Make the control script executable chmod +x claude.sh @@ -55,62 +57,77 @@ chmod +x claude.sh ## Usage +### CLI mode + ```bash # Build images, start proxy, launch Claude Code interactively ./claude.sh start -# Same as start but skips image rebuild (faster on subsequent runs) +# Start proxy if needed, launch Claude Code (faster on subsequent runs) ./claude.sh run -# Stop and remove all containers (proxy + any running sessions) -./claude.sh stop - -# Rebuild images without cache (e.g. after Claude Code updates) -./claude.sh update - -# Tail proxy access logs -./claude.sh logs - -# Show container status -./claude.sh status - -# Open a debug bash shell inside the Claude container -./claude.sh shell -``` - -### Working with host files - -By default, Claude's workspace is a named Docker volume (`claude-secure-workspace`) — fully isolated from the host. - -To mount a specific host directory: - -```bash +# Mount a host directory as the workspace WORKSPACE_DIR=$HOME/myproject ./claude.sh run ``` -The directory is mounted at `/workspace` inside the container. +### Web interface + +Serves Claude Code as a browser terminal via [ttyd](https://github.com/tsl0922/ttyd), protected by HTTP basic auth. + +```bash +# 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 --publish 7681:7681/tcp + +# Stop web interface (keeps proxy running) +./claude.sh web-stop +``` + +### Other commands + +```bash +./claude.sh stop # Stop and remove all containers +./claude.sh update # Rebuild images without cache +./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 +``` + +### Workspace + +| Mode | Default | Override | +|---|---|---| +| CLI (`run`/`start`) | Named Docker volume (isolated) | `WORKSPACE_DIR=/path ./claude.sh run` | +| Web (`web`) | Named Docker volume (`claude-web-workspace`) | Edit `docker-compose.yml` volumes | ## Egress allowlist Edit `proxy/squid.conf` and add domains to the `allowed_sites` ACL: -```squid +``` acl allowed_sites dstdomain api.anthropic.com acl allowed_sites dstdomain statsig.anthropic.com -# acl allowed_sites dstdomain api.github.com # uncomment if needed +# acl allowed_sites dstdomain api.github.com # acl allowed_sites dstdomain registry.npmjs.org ``` -Rebuild the proxy after changes: +Rebuild after changes: ```bash -docker compose -p claude-secure build proxy +./claude.sh update ./claude.sh stop && ./claude.sh start ``` ## Security controls -| Control | Claude container | Proxy container | +| Control | claude / webui | proxy | |---|---|---| | Non-root user | UID 1000 (`claude`) | UID 13 (`proxy`) | | `no-new-privileges` | yes | yes | @@ -118,3 +135,4 @@ docker compose -p claude-secure build proxy | Direct internet access | no (`internal` network only) | allowlisted only | | Host filesystem | no mounts by default | none | | Docker socket | not mounted | not mounted | +| Web auth | basic auth (ttyd `--credential`) | n/a | diff --git a/claude.sh b/claude.sh index 52bcc3a..a302548 100755 --- a/claude.sh +++ b/claude.sh @@ -118,43 +118,76 @@ cmd_shell() { dc run --rm --entrypoint /bin/bash $(workspace_flag) claude } +cmd_web() { + check_deps + load_env + if [[ -z "${WEBUI_PASSWORD:-}" ]]; then + error "WEBUI_PASSWORD is not set. Add it to .env before starting the web interface." + exit 1 + fi + info "Building images..." + dc build + info "Starting proxy and web interface..." + dc up -d webui + local port=7681 + info "Web interface is up → http://0.0.0.0:${port}" + info "Credentials: ${WEBUI_USER:-claude} / [WEBUI_PASSWORD]" + warn "To reach it from outside this host, publish the port:" + warn " sbx ports --publish ${port}:${port}/tcp" +} + +cmd_web_stop() { + check_deps + info "Stopping web interface..." + dc stop webui + dc rm -f webui +} + cmd_help() { cat < [args] Commands: - start [args] Build images, start proxy, launch Claude Code - run [args] Start proxy if needed, launch Claude Code - stop Stop and remove all containers - update Rebuild images without cache - logs [svc] Tail logs (default: proxy) - status Show container status - shell Open a bash shell in the Claude container (debug) - help Show this message + start [args] Build images, start proxy, launch Claude Code (CLI) + run [args] Start proxy if needed, launch Claude Code (CLI) + web Build images, start proxy + web interface (browser terminal) + web-stop Stop the web interface (keeps proxy running) + stop Stop and remove all containers + update Rebuild images without cache + logs [svc] Tail logs (default: proxy) + status Show container status + shell Open a bash shell in the Claude container (debug) + help Show this message -Environment variables: - ANTHROPIC_API_KEY Required. Set in .env or exported in your shell. - WORKSPACE_DIR Optional. Absolute path to mount as /workspace. +Environment variables (set in .env or shell): + ANTHROPIC_API_KEY Required for all modes. + WORKSPACE_DIR Optional (CLI mode). Host path to mount as /workspace. Defaults to a named Docker volume (fully isolated). + WEBUI_USER Web interface username (default: claude). + WEBUI_PASSWORD Required for web mode. Basic auth password. Examples: ./claude.sh start WORKSPACE_DIR=\$HOME/myproject ./claude.sh run + ./claude.sh web ./claude.sh logs proxy + ./claude.sh logs webui ./claude.sh shell EOF } # ─── Dispatch ───────────────────────────────────────────────────────────────── case "${1:-help}" in - start) shift; cmd_start "$@" ;; - stop) cmd_stop ;; - run) shift; cmd_run "$@" ;; - update) cmd_update ;; - logs) shift; cmd_logs "${1:-}" ;; - status) cmd_status ;; - shell) cmd_shell ;; - help|-h|--help) cmd_help ;; + start) shift; cmd_start "$@" ;; + stop) cmd_stop ;; + run) shift; cmd_run "$@" ;; + web) cmd_web ;; + web-stop) cmd_web_stop ;; + update) cmd_update ;; + logs) shift; cmd_logs "${1:-}" ;; + status) cmd_status ;; + shell) cmd_shell ;; + help|-h|--help) cmd_help ;; *) error "Unknown command: ${1}" cmd_help diff --git a/docker-compose.yml b/docker-compose.yml index baadf41..89fa2e0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ services: context: . dockerfile: Dockerfile.proxy networks: - - claude-internal # reachable by the claude container + - claude-internal # reachable by claude and webui containers - proxy-external # has outbound internet access restart: unless-stopped security_opt: @@ -21,7 +21,7 @@ services: - /var/spool/squid - /var/log/squid - # ─── Claude Code container ───────────────────────────────────────────────── + # ─── Claude Code CLI container ───────────────────────────────────────────── # No direct internet access. All egress routes through the proxy sidecar. # Run via "docker compose run --rm claude" (managed by claude.sh). claude: @@ -48,6 +48,41 @@ services: # Workspace is injected by claude.sh via --volume flag at run time. # Default: named Docker volume. Override: set WORKSPACE_DIR on the host. + # ─── Claude Code web interface ───────────────────────────────────────────── + # Serves Claude Code as a browser terminal via ttyd (port 7681). + # Protected by HTTP basic auth — set WEBUI_USER / WEBUI_PASSWORD in .env. + # Network isolation is identical to the CLI container. + webui: + build: + context: . + dockerfile: Dockerfile.claude + entrypoint: ["/usr/local/bin/webui-entrypoint.sh"] + depends_on: + proxy: + condition: service_healthy + networks: + - claude-internal # only — no route to the internet + environment: + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + - HTTP_PROXY=http://proxy:3128 + - HTTPS_PROXY=http://proxy:3128 + - ALL_PROXY=http://proxy:3128 + - NO_PROXY=localhost,127.0.0.1 + - WEBUI_USER=${WEBUI_USER:-claude} + - WEBUI_PASSWORD=${WEBUI_PASSWORD:-} + - WEBUI_PORT=7681 + ports: + - "0.0.0.0:7681:7681" + volumes: + - claude-web-workspace:/workspace + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + stdin_open: true + tty: true + restart: unless-stopped + networks: # Internal-only: Docker adds no default gateway → no direct internet route claude-internal: @@ -57,3 +92,7 @@ networks: # External: standard bridge with internet access (proxy only) proxy-external: driver: bridge + +volumes: + # Persistent workspace for the web interface + claude-web-workspace: diff --git a/webui-entrypoint.sh b/webui-entrypoint.sh new file mode 100644 index 0000000..4cf31cd --- /dev/null +++ b/webui-entrypoint.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Entrypoint for the webui service. +# Wraps Claude Code in ttyd (terminal-over-WebSocket) with basic auth. +set -euo pipefail + +: "${WEBUI_PASSWORD:?WEBUI_PASSWORD must be set in .env}" +WEBUI_USER="${WEBUI_USER:-claude}" +WEBUI_PORT="${WEBUI_PORT:-7681}" + +exec ttyd \ + --port "${WEBUI_PORT}" \ + --writable \ + --credential "${WEBUI_USER}:${WEBUI_PASSWORD}" \ + claude From 782370e0145130c91b6dd3dfa172c61f1f29b4e6 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Tue, 14 Apr 2026 22:26:51 +0200 Subject: [PATCH 06/69] update permissions --- claude.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 claude.sh diff --git a/claude.sh b/claude.sh old mode 100755 new mode 100644 From 88805a3c243ac98f4c4ef0ebd31f85bfaceafcca Mon Sep 17 00:00:00 2001 From: docker-claude Date: Tue, 14 Apr 2026 22:40:57 +0200 Subject: [PATCH 07/69] refactor(docker): migrate both images to Alpine Replace node:20-slim/ubuntu:22.04 with node:20-alpine/alpine:3.21. Switch package management from apt to apk (--no-cache, no cleanup layer). Use Alpine addgroup/adduser in claude/Dockerfile. Update proxy to use squid user (Alpine convention) and /var/cache/squid cache path. Fix proxy/Dockerfile COPY path now that context is proxy/. Move webui-entrypoint.sh into claude/ to match its build context. Fix docker-compose.yml webui context to claude/, update proxy tmpfs path. --- CLAUDE.md | 18 +++++++------ Dockerfile.proxy | 25 ------------------- README.md | 8 +++--- claude.sh | 0 Dockerfile.claude => claude/Dockerfile | 13 +++++----- .../webui-entrypoint.sh | 0 docker-compose.yml | 23 ++++++++--------- proxy/Dockerfile | 19 ++++++++++++++ proxy/squid.conf | 4 ++- 9 files changed, 53 insertions(+), 57 deletions(-) delete mode 100644 Dockerfile.proxy mode change 100644 => 100755 claude.sh rename Dockerfile.claude => claude/Dockerfile (72%) rename webui-entrypoint.sh => claude/webui-entrypoint.sh (100%) create mode 100644 proxy/Dockerfile diff --git a/CLAUDE.md b/CLAUDE.md index 6c93531..547181c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,13 +10,13 @@ This file provides context and guidance for working with this project. Three containers managed by Docker Compose: -- **`claude`** — Claude Code CLI, non-root (UID 1000), isolated to an internal-only Docker network -- **`webui`** — Claude Code as a browser terminal (ttyd on port 7681), same image as `claude`, non-root (UID 1000), same network isolation, basic auth required -- **`proxy`** — Squid forward proxy, non-root (UID 13), bridges the internal network to the internet with an egress allowlist +- **`claude`** — Claude Code CLI (`node:20-alpine`), non-root (UID 1000), isolated to an internal-only Docker network +- **`webui`** — Claude Code as a browser terminal via ttyd (`node:20-alpine`), non-root (UID 1000), same network isolation, basic auth required +- **`proxy`** — Squid forward proxy (`alpine:3.21`), `squid` user, bridges the internal network to the internet with an egress allowlist Key Docker network property: `claude-internal` has `internal: true`, meaning Docker adds no default gateway. The `claude` and `webui` containers physically cannot reach the internet without going through the `proxy` container. -The `webui` service reuses `Dockerfile.claude`. Its entrypoint (`webui-entrypoint.sh`) starts `ttyd --credential user:pass claude` instead of `claude` directly. +The `webui` service reuses `claude/Dockerfile`. Its entrypoint (`claude/webui-entrypoint.sh`) starts `ttyd --credential user:pass claude` instead of `claude` directly. ## File Structure @@ -24,10 +24,11 @@ The `webui` service reuses `Dockerfile.claude`. Its entrypoint (`webui-entrypoin docker-claude/ ├── claude.sh # Control script: start/stop/run/web/web-stop/update/logs/status/shell ├── docker-compose.yml # Service definitions and network topology -├── Dockerfile.claude # Claude Code + ttyd container (node:20-slim, UID 1000) -├── Dockerfile.proxy # Squid proxy sidecar (ubuntu:22.04, UID 13) -├── webui-entrypoint.sh # Entrypoint for webui service: starts ttyd wrapping claude +├── claude/ +│ ├── Dockerfile # Claude Code + ttyd (node:20-alpine, UID 1000) +│ └── webui-entrypoint.sh # Entrypoint for webui: starts ttyd wrapping claude ├── proxy/ +│ ├── Dockerfile # Squid proxy sidecar (alpine:3.21, squid user) │ └── squid.conf # Squid ACL config — egress allowlist lives here ├── .env.example # Template for ANTHROPIC_API_KEY, WEBUI_PASSWORD, etc. ├── .gitignore # Excludes .env and logs @@ -48,7 +49,8 @@ cp .env.example .env # set ANTHROPIC_API_KEY (and WEBUI_PASSWORD for web mo ## Coding Standards - Shell scripts use `set -euo pipefail` -- Dockerfiles use `--no-install-recommends` and clean apt caches in the same layer +- Dockerfiles use Alpine (`node:20-alpine`, `alpine:3.21`) for minimal attack surface +- Alpine packages use `apk add --no-cache`; no apt cache cleanup layer needed - No capabilities granted; `no-new-privileges` on all containers - `.env` is never committed (enforced by `.gitignore` and `.dockerignore`) - Commit messages follow **Angular format**: `type(scope): summary` diff --git a/Dockerfile.proxy b/Dockerfile.proxy deleted file mode 100644 index cfec906..0000000 --- a/Dockerfile.proxy +++ /dev/null @@ -1,25 +0,0 @@ -FROM ubuntu:22.04 - -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update && apt-get install -y --no-install-recommends \ - squid \ - && rm -rf /var/lib/apt/lists/* - -# Give the proxy system user (UID 13) ownership of all Squid paths -RUN mkdir -p /var/spool/squid /var/log/squid \ - && chown -R proxy:proxy /var/spool/squid /var/log/squid /etc/squid - -COPY --chown=proxy:proxy proxy/squid.conf /etc/squid/squid.conf - -USER proxy - -# Initialise cache directories as the proxy user -RUN squid -N -f /etc/squid/squid.conf -z 2>/dev/null || true - -EXPOSE 3128 - -HEALTHCHECK --interval=10s --timeout=5s --retries=3 \ - CMD /bin/bash -c 'echo >/dev/tcp/127.0.0.1/3128' - -CMD ["squid", "-N", "-f", "/etc/squid/squid.conf"] diff --git a/README.md b/README.md index 1fd96a6..2da158b 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,9 @@ Runs [Claude Code](https://claude.ai/code) inside an isolated Docker environment └──────────────────────────────────────────────────────────┘ ``` -- **`claude`** — Claude Code CLI, UID 1000, on `claude-internal` only -- **`webui`** — Claude Code in a browser terminal (ttyd), UID 1000, on `claude-internal` only, port 7681 -- **`proxy`** — Squid forward proxy, UID 13, bridges `claude-internal` ↔ internet with egress allowlist +- **`claude`** — Claude Code CLI (`node:20-alpine`), UID 1000, on `claude-internal` only +- **`webui`** — Claude Code in a browser terminal via ttyd (`node:20-alpine`), UID 1000, on `claude-internal` only, port 7681 +- **`proxy`** — Squid forward proxy (`alpine:3.21`), bridges `claude-internal` ↔ internet with egress allowlist - **`claude-internal`** — `internal: true`; no default gateway, containers cannot reach the internet directly - **`proxy-external`** — Standard bridge; proxy sidecar only @@ -129,7 +129,7 @@ Rebuild after changes: | Control | claude / webui | proxy | |---|---|---| -| Non-root user | UID 1000 (`claude`) | UID 13 (`proxy`) | +| Non-root user | UID 1000 (`claude`) | `squid` user | | `no-new-privileges` | yes | yes | | All capabilities dropped | yes | yes | | Direct internet access | no (`internal` network only) | allowlisted only | diff --git a/claude.sh b/claude.sh old mode 100644 new mode 100755 diff --git a/Dockerfile.claude b/claude/Dockerfile similarity index 72% rename from Dockerfile.claude rename to claude/Dockerfile index 9757f8a..42b2cf7 100644 --- a/Dockerfile.claude +++ b/claude/Dockerfile @@ -1,20 +1,19 @@ -FROM node:20-slim +FROM node:20-alpine -# Install minimal runtime dependencies -RUN apt-get update && apt-get install -y --no-install-recommends \ +# Install runtime dependencies +RUN apk add --no-cache \ git \ curl \ ca-certificates \ bash \ - ttyd \ - && rm -rf /var/lib/apt/lists/* + ttyd # Entrypoint used by the webui service (ttyd wrapping claude) COPY --chmod=755 webui-entrypoint.sh /usr/local/bin/webui-entrypoint.sh # Create non-root user -RUN groupadd -g 1000 claude \ - && useradd -u 1000 -g claude -m -s /bin/bash claude +RUN addgroup -g 1000 claude \ + && adduser -u 1000 -G claude -s /bin/bash -D claude # Install Claude Code globally (runs as root for npm -g, then drops) RUN npm install -g @anthropic-ai/claude-code diff --git a/webui-entrypoint.sh b/claude/webui-entrypoint.sh similarity index 100% rename from webui-entrypoint.sh rename to claude/webui-entrypoint.sh diff --git a/docker-compose.yml b/docker-compose.yml index 89fa2e0..8d6c1ab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,15 +1,14 @@ services: - # ─── Proxy sidecar ───────────────────────────────────────────────────────── # Bridges the isolated internal network to the internet. # Enforces an egress allowlist — see proxy/squid.conf. proxy: build: - context: . - dockerfile: Dockerfile.proxy + context: proxy + dockerfile: Dockerfile networks: - - claude-internal # reachable by claude and webui containers - - proxy-external # has outbound internet access + - claude-internal # reachable by claude and webui containers + - proxy-external # has outbound internet access restart: unless-stopped security_opt: - no-new-privileges:true @@ -18,7 +17,7 @@ services: read_only: true tmpfs: - /tmp - - /var/spool/squid + - /var/cache/squid - /var/log/squid # ─── Claude Code CLI container ───────────────────────────────────────────── @@ -26,13 +25,13 @@ services: # Run via "docker compose run --rm claude" (managed by claude.sh). claude: build: - context: . - dockerfile: Dockerfile.claude + context: claude/ + dockerfile: Dockerfile depends_on: proxy: condition: service_healthy networks: - - claude-internal # only — no route to the internet + - claude-internal # only — no route to the internet environment: - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - HTTP_PROXY=http://proxy:3128 @@ -54,14 +53,14 @@ services: # Network isolation is identical to the CLI container. webui: build: - context: . - dockerfile: Dockerfile.claude + context: claude/ + dockerfile: Dockerfile entrypoint: ["/usr/local/bin/webui-entrypoint.sh"] depends_on: proxy: condition: service_healthy networks: - - claude-internal # only — no route to the internet + - claude-internal # only — no route to the internet environment: - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - HTTP_PROXY=http://proxy:3128 diff --git a/proxy/Dockerfile b/proxy/Dockerfile new file mode 100644 index 0000000..7382d3c --- /dev/null +++ b/proxy/Dockerfile @@ -0,0 +1,19 @@ +FROM alpine:3.21 + +# squid: proxy. netcat-openbsd: health check +RUN apk add --no-cache squid netcat-openbsd + +# squid user is created by the package (apk add squid) +RUN mkdir -p /var/cache/squid /var/log/squid \ + && chown -R squid:squid /var/cache/squid /var/log/squid /etc/squid + +COPY --chown=squid:squid squid.conf /etc/squid/squid.conf + +USER squid + +EXPOSE 3128 + +HEALTHCHECK --interval=10s --timeout=5s --retries=3 \ + CMD nc -z 127.0.0.1 3128 || exit 1 + +CMD ["squid", "-N", "-f", "/etc/squid/squid.conf"] diff --git a/proxy/squid.conf b/proxy/squid.conf index 6ef039f..9482ad1 100644 --- a/proxy/squid.conf +++ b/proxy/squid.conf @@ -14,7 +14,7 @@ cache_store_log none # ─── No disk cache ──────────────────────────────────────────────────────────── cache deny all -coredump_dir /var/spool/squid +coredump_dir /var/cache/squid # ─── ACL Definitions ────────────────────────────────────────────────────────── acl SSL_ports port 443 @@ -26,6 +26,8 @@ acl CONNECT method CONNECT # Add domains here as needed. Leading dot matches all subdomains. acl allowed_sites dstdomain api.anthropic.com acl allowed_sites dstdomain statsig.anthropic.com +acl allowed_sites dstdomain localhost +acl allowed_sites dstdomain .local # ─── Access rules ───────────────────────────────────────────────────────────── # Block requests to non-standard ports From ba3730a24d0d7db6ee24da9e540a90e06adc9919 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Tue, 14 Apr 2026 22:47:04 +0200 Subject: [PATCH 08/69] feat(auth): support subscription login alongside API key Make ANTHROPIC_API_KEY optional. Add CLAUDE_CODE_OAUTH_TOKEN pass-through for headless token-based auth (claude setup-token). When neither is set, Claude Code falls back to browser OAuth on port 54545. Add claude-config named volume mounted at ~/.claude/ in both claude and webui services so credentials persist across container runs. Pre-create ~/.claude/ in the Dockerfile so the volume is initialised with correct ownership. Add --service-ports to docker compose run calls to publish port 54545 during CLI sessions. --- .env.example | 20 +++++++++++++++++--- CLAUDE.md | 5 +++++ README.md | 36 ++++++++++++++++++++++++++++++++++-- claude.sh | 17 ++++++++++------- claude/Dockerfile | 8 ++++++-- docker-compose.yml | 23 ++++++++++++++++++----- 6 files changed, 90 insertions(+), 19 deletions(-) mode change 100755 => 100644 claude.sh diff --git a/.env.example b/.env.example index 00a8187..502aa0d 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,27 @@ # Copy this file to .env and fill in your values. # .env is git-ignored — never commit it. -# Required: your Anthropic API key -ANTHROPIC_API_KEY=sk-ant-... +# ─── Authentication (choose one) ────────────────────────────────────────────── + +# Option 1: Anthropic API key +# ANTHROPIC_API_KEY=sk-ant-... + +# Option 2: OAuth token from a Claude.ai subscription (1-year validity) +# Generate with: claude setup-token (run on your host, not inside the container) +# CLAUDE_CODE_OAUTH_TOKEN=... + +# Option 3: No key set — Claude Code will prompt for browser login on first run. +# Port 54545 must be reachable from your browser for the OAuth callback. +# Run: sbx ports --publish 54545:54545/tcp + +# ─── Workspace (CLI mode only) ──────────────────────────────────────────────── # Optional: mount a host directory as /workspace inside the Claude container. # If unset, a named Docker volume is used (fully isolated from the host). # WORKSPACE_DIR=/absolute/path/to/your/project -# Web interface credentials (required for ./claude.sh web) +# ─── Web interface ──────────────────────────────────────────────────────────── + +# Required for ./claude.sh web # WEBUI_USER=claude # WEBUI_PASSWORD=changeme diff --git a/CLAUDE.md b/CLAUDE.md index 547181c..41d07a2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,6 +18,11 @@ Key Docker network property: `claude-internal` has `internal: true`, meaning Doc The `webui` service reuses `claude/Dockerfile`. Its entrypoint (`claude/webui-entrypoint.sh`) starts `ttyd --credential user:pass claude` instead of `claude` directly. +Auth supports three modes (checked at startup by `claude.sh`): +- `ANTHROPIC_API_KEY` — API key +- `CLAUDE_CODE_OAUTH_TOKEN` — 1-year token from `claude setup-token` (headless-friendly) +- Neither set — Claude Code prompts for browser login on first run; port 54545 is published for the OAuth callback. Credentials persist in the `claude-config` named volume. + ## File Structure ``` diff --git a/README.md b/README.md index 2da158b..769d2f4 100644 --- a/README.md +++ b/README.md @@ -47,14 +47,46 @@ Runs [Claude Code](https://claude.ai/code) inside an isolated Docker environment # 1. Clone / copy this repo git clone docker-claude && cd docker-claude -# 2. Configure credentials +# 2. Configure credentials (see Authentication below) cp .env.example .env -$EDITOR .env # set ANTHROPIC_API_KEY (and WEBUI_PASSWORD if using web mode) +$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 +```bash +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: +```bash +claude setup-token +``` +Then set the printed token in `.env`: +```bash +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: + +```bash +sbx ports --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 diff --git a/claude.sh b/claude.sh old mode 100755 new mode 100644 index a302548..de90c7f --- a/claude.sh +++ b/claude.sh @@ -32,10 +32,13 @@ load_env() { # shellcheck disable=SC1090 set -a; source "$env_file"; set +a fi - if [[ -z "${ANTHROPIC_API_KEY:-}" ]]; then - error "ANTHROPIC_API_KEY is not set." - error "Copy .env.example → .env and add your key, or export it in your shell." - exit 1 + if [[ -z "${ANTHROPIC_API_KEY:-}" && -z "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]]; then + warn "No ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN found." + warn "Claude Code will prompt you to authenticate on first run." + warn " Option 1 (API key): set ANTHROPIC_API_KEY in .env" + warn " Option 2 (token): run 'claude setup-token' and set CLAUDE_CODE_OAUTH_TOKEN in .env" + warn " Option 3 (browser): run './claude.sh run' and follow the login prompt;" + warn " port 54545 must be reachable from your browser." fi } @@ -72,7 +75,7 @@ cmd_start() { dc up -d proxy # no-op if already healthy; compose waits via depends_on info "Launching Claude Code..." # shellcheck disable=SC2046 - dc run --rm $(workspace_flag) claude "$@" + dc run --rm --service-ports $(workspace_flag) claude "$@" } cmd_stop() { @@ -89,7 +92,7 @@ cmd_run() { dc up -d proxy info "Launching Claude Code..." # shellcheck disable=SC2046 - dc run --rm $(workspace_flag) claude "$@" + dc run --rm --service-ports $(workspace_flag) claude "$@" } cmd_update() { @@ -115,7 +118,7 @@ cmd_shell() { load_env warn "Opening debug shell inside Claude container (non-Claude entrypoint)." # shellcheck disable=SC2046 - dc run --rm --entrypoint /bin/bash $(workspace_flag) claude + dc run --rm --service-ports --entrypoint /bin/bash $(workspace_flag) claude } cmd_web() { diff --git a/claude/Dockerfile b/claude/Dockerfile index 42b2cf7..466933b 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -18,8 +18,12 @@ RUN addgroup -g 1000 claude \ # Install Claude Code globally (runs as root for npm -g, then drops) RUN npm install -g @anthropic-ai/claude-code -# Workspace directory owned by claude user -RUN mkdir -p /workspace && chown claude:claude /workspace +# Workspace and Claude config dir — both owned by claude user. +# Pre-creating ~/.claude ensures the named volume is initialised with the +# correct ownership when first mounted (Docker copies image content into +# an empty named volume on first use). +RUN mkdir -p /workspace /home/claude/.claude \ + && chown -R claude:claude /workspace /home/claude/.claude USER claude WORKDIR /workspace diff --git a/docker-compose.yml b/docker-compose.yml index 8d6c1ab..e71a964 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,7 @@ services: # ─── Claude Code CLI container ───────────────────────────────────────────── # No direct internet access. All egress routes through the proxy sidecar. - # Run via "docker compose run --rm claude" (managed by claude.sh). + # Run via "docker compose run --rm --service-ports claude" (managed by claude.sh). claude: build: context: claude/ @@ -33,19 +33,25 @@ services: networks: - claude-internal # only — no route to the internet environment: - - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} + - CLAUDE_CODE_OAUTH_TOKEN=${CLAUDE_CODE_OAUTH_TOKEN:-} - HTTP_PROXY=http://proxy:3128 - HTTPS_PROXY=http://proxy:3128 - ALL_PROXY=http://proxy:3128 - NO_PROXY=localhost,127.0.0.1 + ports: + # OAuth callback — required for browser-based login (claude login) + - "0.0.0.0:54545:54545" + volumes: + - claude-config:/home/claude/.claude + # Workspace is injected by claude.sh via --volume flag at run time. + # Default: named Docker volume. Override: set WORKSPACE_DIR on the host. security_opt: - no-new-privileges:true cap_drop: - ALL stdin_open: true tty: true - # Workspace is injected by claude.sh via --volume flag at run time. - # Default: named Docker volume. Override: set WORKSPACE_DIR on the host. # ─── Claude Code web interface ───────────────────────────────────────────── # Serves Claude Code as a browser terminal via ttyd (port 7681). @@ -62,7 +68,8 @@ services: networks: - claude-internal # only — no route to the internet environment: - - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} + - CLAUDE_CODE_OAUTH_TOKEN=${CLAUDE_CODE_OAUTH_TOKEN:-} - HTTP_PROXY=http://proxy:3128 - HTTPS_PROXY=http://proxy:3128 - ALL_PROXY=http://proxy:3128 @@ -72,7 +79,10 @@ services: - WEBUI_PORT=7681 ports: - "0.0.0.0:7681:7681" + # OAuth callback — required for browser-based login (claude login) + - "0.0.0.0:54545:54545" volumes: + - claude-config:/home/claude/.claude - claude-web-workspace:/workspace security_opt: - no-new-privileges:true @@ -93,5 +103,8 @@ networks: driver: bridge volumes: + # Persists Claude Code auth credentials (~/.claude/) across container runs. + # Shared between the CLI and web interface so login carries over. + claude-config: # Persistent workspace for the web interface claude-web-workspace: From 0800e4a0847a2aaf8d9ead693ef335f77f35d0f1 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Tue, 14 Apr 2026 22:49:42 +0200 Subject: [PATCH 09/69] fix(claude): use gid/uid 1001 for claude user node:20-alpine reserves gid/uid 1000 for its built-in node user, causing addgroup to fail. Shift claude to 1001. --- claude/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/claude/Dockerfile b/claude/Dockerfile index 466933b..be509a0 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -11,9 +11,9 @@ RUN apk add --no-cache \ # Entrypoint used by the webui service (ttyd wrapping claude) COPY --chmod=755 webui-entrypoint.sh /usr/local/bin/webui-entrypoint.sh -# Create non-root user -RUN addgroup -g 1000 claude \ - && adduser -u 1000 -G claude -s /bin/bash -D claude +# Create non-root user (node:20-alpine reserves gid/uid 1000 for the node user) +RUN addgroup -g 1001 claude \ + && adduser -u 1001 -G claude -s /bin/bash -D claude # Install Claude Code globally (runs as root for npm -g, then drops) RUN npm install -g @anthropic-ai/claude-code From 1c489f86362ad34893860d5ed5304b71e29b6efc Mon Sep 17 00:00:00 2001 From: docker-claude Date: Tue, 14 Apr 2026 22:50:59 +0200 Subject: [PATCH 10/69] refactor(claude): use built-in node user instead of custom claude user Drop the addgroup/adduser layer entirely. node:20-alpine already ships a node user at uid/gid 1000. Update chown and USER directives, and update the claude-config volume mount path to /home/node/.claude. --- CLAUDE.md | 4 ++-- README.md | 6 +++--- claude/Dockerfile | 14 +++++--------- docker-compose.yml | 4 ++-- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 41d07a2..1abc17e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,8 +10,8 @@ This file provides context and guidance for working with this project. Three containers managed by Docker Compose: -- **`claude`** — Claude Code CLI (`node:20-alpine`), non-root (UID 1000), isolated to an internal-only Docker network -- **`webui`** — Claude Code as a browser terminal via ttyd (`node:20-alpine`), non-root (UID 1000), same network isolation, basic auth required +- **`claude`** — Claude Code CLI (`node:20-alpine`), runs as the built-in `node` user (UID 1000), isolated to an internal-only Docker network +- **`webui`** — Claude Code as a browser terminal via ttyd (`node:20-alpine`), `node` user (UID 1000), same network isolation, basic auth required - **`proxy`** — Squid forward proxy (`alpine:3.21`), `squid` user, bridges the internal network to the internet with an egress allowlist Key Docker network property: `claude-internal` has `internal: true`, meaning Docker adds no default gateway. The `claude` and `webui` containers physically cannot reach the internet without going through the `proxy` container. diff --git a/README.md b/README.md index 769d2f4..c729c70 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,8 @@ Runs [Claude Code](https://claude.ai/code) inside an isolated Docker environment └──────────────────────────────────────────────────────────┘ ``` -- **`claude`** — Claude Code CLI (`node:20-alpine`), UID 1000, on `claude-internal` only -- **`webui`** — Claude Code in a browser terminal via ttyd (`node:20-alpine`), UID 1000, on `claude-internal` only, port 7681 +- **`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-internal`** — `internal: true`; no default gateway, containers cannot reach the internet directly - **`proxy-external`** — Standard bridge; proxy sidecar only @@ -161,7 +161,7 @@ Rebuild after changes: | Control | claude / webui | proxy | |---|---|---| -| Non-root user | UID 1000 (`claude`) | `squid` user | +| 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 | diff --git a/claude/Dockerfile b/claude/Dockerfile index be509a0..1a70a05 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -11,21 +11,17 @@ RUN apk add --no-cache \ # Entrypoint used by the webui service (ttyd wrapping claude) COPY --chmod=755 webui-entrypoint.sh /usr/local/bin/webui-entrypoint.sh -# Create non-root user (node:20-alpine reserves gid/uid 1000 for the node user) -RUN addgroup -g 1001 claude \ - && adduser -u 1001 -G claude -s /bin/bash -D claude - -# Install Claude Code globally (runs as root for npm -g, then drops) +# Install Claude Code globally RUN npm install -g @anthropic-ai/claude-code -# Workspace and Claude config dir — both owned by claude user. +# Workspace and Claude config dir — owned by the built-in node user (uid 1000). # Pre-creating ~/.claude ensures the named volume is initialised with the # correct ownership when first mounted (Docker copies image content into # an empty named volume on first use). -RUN mkdir -p /workspace /home/claude/.claude \ - && chown -R claude:claude /workspace /home/claude/.claude +RUN mkdir -p /workspace /home/node/.claude \ + && chown -R node:node /workspace /home/node/.claude -USER claude +USER node WORKDIR /workspace # Proxy traffic through sidecar — override at runtime if needed diff --git a/docker-compose.yml b/docker-compose.yml index e71a964..bc7df0d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,7 +43,7 @@ services: # OAuth callback — required for browser-based login (claude login) - "0.0.0.0:54545:54545" volumes: - - claude-config:/home/claude/.claude + - claude-config:/home/node/.claude # Workspace is injected by claude.sh via --volume flag at run time. # Default: named Docker volume. Override: set WORKSPACE_DIR on the host. security_opt: @@ -82,7 +82,7 @@ services: # OAuth callback — required for browser-based login (claude login) - "0.0.0.0:54545:54545" volumes: - - claude-config:/home/claude/.claude + - claude-config:/home/node/.claude - claude-web-workspace:/workspace security_opt: - no-new-privileges:true From 3adc97d9016ca2da46ec029ac0a2f20ba37d4e9a Mon Sep 17 00:00:00 2001 From: docker-claude Date: Tue, 14 Apr 2026 22:55:02 +0200 Subject: [PATCH 11/69] feat(policy): restrict available models to sonnet, opus, haiku Add /etc/claude-code/managed-settings.json with availableModels set to the three Anthropic model families. The file is root-owned inside the container so the node user cannot modify it. Managed settings cannot be bypassed via --model flag, /model command, or ANTHROPIC_MODEL env var. --- claude/Dockerfile | 4 ++++ claude/managed-settings.json | 3 +++ 2 files changed, 7 insertions(+) create mode 100644 claude/managed-settings.json diff --git a/claude/Dockerfile b/claude/Dockerfile index 1a70a05..3025650 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -11,6 +11,10 @@ RUN apk add --no-cache \ # Entrypoint used by the webui service (ttyd wrapping claude) COPY --chmod=755 webui-entrypoint.sh /usr/local/bin/webui-entrypoint.sh +# System-level Claude Code policy — owned by root, not writable by the node user. +# Restricts available models; cannot be bypassed via CLI flags or env vars. +COPY managed-settings.json /etc/claude-code/managed-settings.json + # Install Claude Code globally RUN npm install -g @anthropic-ai/claude-code diff --git a/claude/managed-settings.json b/claude/managed-settings.json new file mode 100644 index 0000000..f43cb85 --- /dev/null +++ b/claude/managed-settings.json @@ -0,0 +1,3 @@ +{ + "availableModels": ["sonnet", "opus", "haiku"] +} From 6410f22f1da9f670d897307bc6d3822e9887826b Mon Sep 17 00:00:00 2001 From: docker-claude Date: Tue, 14 Apr 2026 22:57:29 +0200 Subject: [PATCH 12/69] feat(policy): allow bash and file modification tools without prompting Add permissions.allow to managed-settings.json for Bash(*), Edit(*), and Write(*). Claude Code will not prompt for confirmation on shell commands or file writes inside the container. --- claude/managed-settings.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/claude/managed-settings.json b/claude/managed-settings.json index f43cb85..9dfc165 100644 --- a/claude/managed-settings.json +++ b/claude/managed-settings.json @@ -1,3 +1,10 @@ { - "availableModels": ["sonnet", "opus", "haiku"] + "availableModels": ["sonnet", "opus", "haiku"], + "permissions": { + "allow": [ + "Bash(*)", + "Edit(*)", + "Write(*)" + ] + } } From c65ed1565349815a59db6d8de7ec84013fd25b20 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Tue, 14 Apr 2026 22:59:25 +0200 Subject: [PATCH 13/69] refactor(policy): rename managed-settings.json to settings.json --- claude/Dockerfile | 2 +- claude/{managed-settings.json => settings.json} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename claude/{managed-settings.json => settings.json} (100%) diff --git a/claude/Dockerfile b/claude/Dockerfile index 3025650..e00159d 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -13,7 +13,7 @@ COPY --chmod=755 webui-entrypoint.sh /usr/local/bin/webui-entrypoint.sh # System-level Claude Code policy — owned by root, not writable by the node user. # Restricts available models; cannot be bypassed via CLI flags or env vars. -COPY managed-settings.json /etc/claude-code/managed-settings.json +COPY settings.json /etc/claude-code/managed-settings.json # Install Claude Code globally RUN npm install -g @anthropic-ai/claude-code diff --git a/claude/managed-settings.json b/claude/settings.json similarity index 100% rename from claude/managed-settings.json rename to claude/settings.json From e19d4eb0a31e4106038b50778d42c5e1efa95c2e Mon Sep 17 00:00:00 2001 From: docker-claude Date: Tue, 14 Apr 2026 23:09:42 +0200 Subject: [PATCH 14/69] feat(mcp): add GitHub, GitLab, Jira, and Confluence MCP servers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Install four MCP servers globally in the claude image: @modelcontextprotocol/server-github → mcp-server-github @yoda.digital/gitlab-mcp-server → gitlab-mcp-server @aashari/mcp-server-atlassian-jira → mcp-atlassian-jira @aashari/mcp-server-atlassian-confluence → mcp-atlassian-confluence Wire them in managed-settings.json via mcpServers with env var pass-through. Jira and Confluence share ATLASSIAN_* credentials. Add api.github.com, .gitlab.com, .atlassian.net to the squid allowlist. All credentials are optional — servers are skipped if the relevant env vars are unset. --- .env.example | 14 ++++++++++++++ claude/Dockerfile | 7 +++++++ claude/settings.json | 41 ++++++++++++++++++++++++++++++++++++----- docker-compose.yml | 14 ++++++++++++++ proxy/squid.conf | 4 ++++ 5 files changed, 75 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index 502aa0d..935ccb8 100644 --- a/.env.example +++ b/.env.example @@ -25,3 +25,17 @@ # Required for ./claude.sh web # WEBUI_USER=claude # WEBUI_PASSWORD=changeme + +# ─── MCP servers (all optional) ─────────────────────────────────────────────── + +# GitHub — PAT with repo scope +# GITHUB_TOKEN=ghp_... + +# GitLab — PAT with api scope; GITLAB_URL defaults to https://gitlab.com +# GITLAB_TOKEN=glpat_... +# GITLAB_URL=https://gitlab.com + +# Jira + Confluence — shared Atlassian credentials +# ATLASSIAN_SITE_NAME=your-company # subdomain of .atlassian.net +# ATLASSIAN_USER_EMAIL=you@example.com +# ATLASSIAN_API_TOKEN=... # https://id.atlassian.com/manage-profile/security/api-tokens diff --git a/claude/Dockerfile b/claude/Dockerfile index e00159d..b48ae3a 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -18,6 +18,13 @@ COPY settings.json /etc/claude-code/managed-settings.json # Install Claude Code globally RUN npm install -g @anthropic-ai/claude-code +# Install MCP servers globally — entry points land in /usr/local/lib/node_modules/ +RUN npm install -g \ + @modelcontextprotocol/server-github \ + @yoda.digital/gitlab-mcp-server \ + @aashari/mcp-server-atlassian-jira \ + @aashari/mcp-server-atlassian-confluence + # Workspace and Claude config dir — owned by the built-in node user (uid 1000). # Pre-creating ~/.claude ensures the named volume is initialised with the # correct ownership when first mounted (Docker copies image content into diff --git a/claude/settings.json b/claude/settings.json index 9dfc165..4e2033e 100644 --- a/claude/settings.json +++ b/claude/settings.json @@ -1,10 +1,41 @@ { "availableModels": ["sonnet", "opus", "haiku"], "permissions": { - "allow": [ - "Bash(*)", - "Edit(*)", - "Write(*)" - ] + "allow": ["Bash(*)", "Edit(*)", "Write(*)"], + "deny": ["Bash(curl *)", "Read(.*env*)"], + "env": { + "CLAUDE_CODE_ENABLE_TELEMETRY": "0" + } + }, + "mcpServers": { + "github": { + "command": "mcp-server-github", + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}" + } + }, + "gitlab": { + "command": "gitlab-mcp-server", + "env": { + "GITLAB_PERSONAL_ACCESS_TOKEN": "${GITLAB_TOKEN}", + "GITLAB_URL": "${GITLAB_URL}" + } + }, + "jira": { + "command": "mcp-atlassian-jira", + "env": { + "ATLASSIAN_SITE_NAME": "${ATLASSIAN_SITE_NAME}", + "ATLASSIAN_USER_EMAIL": "${ATLASSIAN_USER_EMAIL}", + "ATLASSIAN_API_TOKEN": "${ATLASSIAN_API_TOKEN}" + } + }, + "confluence": { + "command": "mcp-atlassian-confluence", + "env": { + "ATLASSIAN_SITE_NAME": "${ATLASSIAN_SITE_NAME}", + "ATLASSIAN_USER_EMAIL": "${ATLASSIAN_USER_EMAIL}", + "ATLASSIAN_API_TOKEN": "${ATLASSIAN_API_TOKEN}" + } + } } } diff --git a/docker-compose.yml b/docker-compose.yml index bc7df0d..753b2c4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,6 +39,13 @@ services: - HTTPS_PROXY=http://proxy:3128 - ALL_PROXY=http://proxy:3128 - NO_PROXY=localhost,127.0.0.1 + # MCP server credentials — all optional; servers are skipped if unset + - GITHUB_TOKEN=${GITHUB_TOKEN:-} + - GITLAB_TOKEN=${GITLAB_TOKEN:-} + - GITLAB_URL=${GITLAB_URL:-https://gitlab.com} + - ATLASSIAN_SITE_NAME=${ATLASSIAN_SITE_NAME:-} + - ATLASSIAN_USER_EMAIL=${ATLASSIAN_USER_EMAIL:-} + - ATLASSIAN_API_TOKEN=${ATLASSIAN_API_TOKEN:-} ports: # OAuth callback — required for browser-based login (claude login) - "0.0.0.0:54545:54545" @@ -74,6 +81,13 @@ services: - HTTPS_PROXY=http://proxy:3128 - ALL_PROXY=http://proxy:3128 - NO_PROXY=localhost,127.0.0.1 + # MCP server credentials — all optional; servers are skipped if unset + - GITHUB_TOKEN=${GITHUB_TOKEN:-} + - GITLAB_TOKEN=${GITLAB_TOKEN:-} + - GITLAB_URL=${GITLAB_URL:-https://gitlab.com} + - ATLASSIAN_SITE_NAME=${ATLASSIAN_SITE_NAME:-} + - ATLASSIAN_USER_EMAIL=${ATLASSIAN_USER_EMAIL:-} + - ATLASSIAN_API_TOKEN=${ATLASSIAN_API_TOKEN:-} - WEBUI_USER=${WEBUI_USER:-claude} - WEBUI_PASSWORD=${WEBUI_PASSWORD:-} - WEBUI_PORT=7681 diff --git a/proxy/squid.conf b/proxy/squid.conf index 9482ad1..cac6ff8 100644 --- a/proxy/squid.conf +++ b/proxy/squid.conf @@ -28,6 +28,10 @@ acl allowed_sites dstdomain api.anthropic.com acl allowed_sites dstdomain statsig.anthropic.com acl allowed_sites dstdomain localhost acl allowed_sites dstdomain .local +# MCP servers +acl allowed_sites dstdomain api.github.com +acl allowed_sites dstdomain .gitlab.com +acl allowed_sites dstdomain .atlassian.net # ─── Access rules ───────────────────────────────────────────────────────────── # Block requests to non-standard ports From 3401fa38a57ef85a241084bd97926b88524a9781 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 08:10:44 +0200 Subject: [PATCH 15/69] refactor(workspace): mount CWD as /workspace instead of named volume Run from the project directory you want to work on; claude.sh mounts it automatically. Removes WORKSPACE_DIR env var support and the named claude-secure-workspace Docker volume. --- .env.example | 6 ------ CLAUDE.md | 8 ++++---- README.md | 16 +++++++--------- claude.sh | 20 +++----------------- docker-compose.yml | 3 +-- 5 files changed, 15 insertions(+), 38 deletions(-) diff --git a/.env.example b/.env.example index 935ccb8..e8c6eea 100644 --- a/.env.example +++ b/.env.example @@ -14,12 +14,6 @@ # Port 54545 must be reachable from your browser for the OAuth callback. # Run: sbx ports --publish 54545:54545/tcp -# ─── Workspace (CLI mode only) ──────────────────────────────────────────────── - -# Optional: mount a host directory as /workspace inside the Claude container. -# If unset, a named Docker volume is used (fully isolated from the host). -# WORKSPACE_DIR=/absolute/path/to/your/project - # ─── Web interface ──────────────────────────────────────────────────────────── # Required for ./claude.sh web diff --git a/CLAUDE.md b/CLAUDE.md index 1abc17e..b9d8ea5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,10 +45,10 @@ docker-claude/ ```bash chmod +x claude.sh -cp .env.example .env # set ANTHROPIC_API_KEY (and WEBUI_PASSWORD for web mode) -./claude.sh start # build + start proxy + launch Claude interactively (CLI) -./claude.sh web # build + start proxy + webui (browser terminal on :7681) -./claude.sh update # rebuild images (no cache) after upstream updates +cp .env.example .env # set ANTHROPIC_API_KEY (and WEBUI_PASSWORD for web mode) +cd /path/to/project && ./claude.sh start # build + start proxy + launch Claude (mounts CWD as /workspace) +./claude.sh web # build + start proxy + webui (browser terminal on :7681) +./claude.sh update # rebuild images (no cache) after upstream updates ``` ## Coding Standards diff --git a/README.md b/README.md index c729c70..6f37c42 100644 --- a/README.md +++ b/README.md @@ -92,14 +92,12 @@ Then run `./claude.sh run` and follow the prompt. Credentials are stored in the ### CLI mode ```bash -# Build images, start proxy, launch Claude Code interactively +# Build images, start proxy, launch Claude Code in the current directory +cd ~/myproject ./claude.sh start # Start proxy if needed, launch Claude Code (faster on subsequent runs) ./claude.sh run - -# Mount a host directory as the workspace -WORKSPACE_DIR=$HOME/myproject ./claude.sh run ``` ### Web interface @@ -134,10 +132,10 @@ sbx ports --publish 7681:7681/tcp ### Workspace -| Mode | Default | Override | -|---|---|---| -| CLI (`run`/`start`) | Named Docker volume (isolated) | `WORKSPACE_DIR=/path ./claude.sh run` | -| Web (`web`) | Named Docker volume (`claude-web-workspace`) | Edit `docker-compose.yml` volumes | +| Mode | Workspace | +|---|---| +| CLI (`run`/`start`) | Current working directory (mounted as `/workspace`) | +| Web (`web`) | Named Docker volume (`claude-web-workspace`) | ## Egress allowlist @@ -165,6 +163,6 @@ Rebuild after changes: | `no-new-privileges` | yes | yes | | All capabilities dropped | yes | yes | | Direct internet access | no (`internal` network only) | allowlisted only | -| Host filesystem | no mounts by default | none | +| Host filesystem | CWD mounted as `/workspace` (CLI only) | none | | Docker socket | not mounted | not mounted | | Web auth | basic auth (ttyd `--credential`) | n/a | diff --git a/claude.sh b/claude.sh index de90c7f..62aad47 100644 --- a/claude.sh +++ b/claude.sh @@ -43,20 +43,9 @@ load_env() { } # ─── Workspace volume resolution ────────────────────────────────────────────── -# Default: named Docker volume (fully isolated). -# Override: export WORKSPACE_DIR=/path/to/project before running. +# Mounts the current working directory as /workspace inside the container. workspace_flag() { - if [[ -n "${WORKSPACE_DIR:-}" ]]; then - local abs - abs="$(realpath "${WORKSPACE_DIR}")" - if [[ ! -d "$abs" ]]; then - error "WORKSPACE_DIR does not exist: $abs" - exit 1 - fi - echo "--volume ${abs}:/workspace:z" - else - echo "--volume ${PROJECT}-workspace:/workspace" - fi + echo "--volume $(pwd):/workspace:z" } # ─── Compose wrapper ────────────────────────────────────────────────────────── @@ -164,14 +153,11 @@ Commands: Environment variables (set in .env or shell): ANTHROPIC_API_KEY Required for all modes. - WORKSPACE_DIR Optional (CLI mode). Host path to mount as /workspace. - Defaults to a named Docker volume (fully isolated). WEBUI_USER Web interface username (default: claude). WEBUI_PASSWORD Required for web mode. Basic auth password. Examples: - ./claude.sh start - WORKSPACE_DIR=\$HOME/myproject ./claude.sh run + cd ~/myproject && ./claude.sh start ./claude.sh web ./claude.sh logs proxy ./claude.sh logs webui diff --git a/docker-compose.yml b/docker-compose.yml index 753b2c4..e215390 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -51,8 +51,7 @@ services: - "0.0.0.0:54545:54545" volumes: - claude-config:/home/node/.claude - # Workspace is injected by claude.sh via --volume flag at run time. - # Default: named Docker volume. Override: set WORKSPACE_DIR on the host. + # Workspace is injected by claude.sh via --volume flag at run time (current directory). security_opt: - no-new-privileges:true cap_drop: From 65ac4c7011f5e222473ca9bb76c65e588021e3bf Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 08:40:50 +0200 Subject: [PATCH 16/69] feat(security): block mounting home and system directories as workspace --- claude.sh | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/claude.sh b/claude.sh index 62aad47..a9267e9 100644 --- a/claude.sh +++ b/claude.sh @@ -44,8 +44,44 @@ load_env() { # ─── Workspace volume resolution ────────────────────────────────────────────── # Mounts the current working directory as /workspace inside the container. +# Refuses to mount the home directory or system directories. workspace_flag() { - echo "--volume $(pwd):/workspace:z" + local cwd + cwd="$(pwd)" + + # Exact-match blocklist — mounting these exposes too much of the host + local -a exact_blocked=( + / + "$HOME" + /root + /home + ) + + # Prefix blocklist — these and any subdirectory are system internals + local -a prefix_blocked=( + /bin /sbin /lib /lib64 + /etc /usr /var + /proc /sys /dev + /boot /run + ) + + for dir in "${exact_blocked[@]}"; do + if [[ "$cwd" == "$dir" ]]; then + error "Refusing to mount $cwd as workspace — too broad." + error "cd into a project subdirectory first." + exit 1 + fi + done + + for dir in "${prefix_blocked[@]}"; do + if [[ "$cwd" == "$dir" || "$cwd" == "$dir/"* ]]; then + error "Refusing to mount $cwd as workspace — system directory." + error "cd into a project subdirectory first." + exit 1 + fi + done + + echo "--volume ${cwd}:/workspace:z" } # ─── Compose wrapper ────────────────────────────────────────────────────────── From c3875397b086503d2bf6133d9d76134cedecff06 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 08:43:09 +0200 Subject: [PATCH 17/69] feat(security): block user home dirs and SSH/PGP key directories from workspace mount --- claude.sh | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/claude.sh b/claude.sh index a9267e9..14ba877 100644 --- a/claude.sh +++ b/claude.sh @@ -44,7 +44,7 @@ load_env() { # ─── Workspace volume resolution ────────────────────────────────────────────── # Mounts the current working directory as /workspace inside the container. -# Refuses to mount the home directory or system directories. +# Refuses to mount home directories, key material, or system directories. workspace_flag() { local cwd cwd="$(pwd)" @@ -57,12 +57,19 @@ workspace_flag() { /home ) - # Prefix blocklist — these and any subdirectory are system internals + # Prefix blocklist — block these paths and all subdirectories. + # Covers system internals and credential/key material. local -a prefix_blocked=( /bin /sbin /lib /lib64 /etc /usr /var /proc /sys /dev /boot /run + # SSH keys + "$HOME/.ssh" + /root/.ssh + # PGP/GPG keys + "$HOME/.gnupg" + /root/.gnupg ) for dir in "${exact_blocked[@]}"; do @@ -73,9 +80,16 @@ workspace_flag() { fi done + # Block any user home directory directly under /home (e.g. /home/alice) + if [[ "$cwd" =~ ^/home/[^/]+$ ]]; then + error "Refusing to mount $cwd as workspace — user home directory." + error "cd into a project subdirectory first." + exit 1 + fi + for dir in "${prefix_blocked[@]}"; do if [[ "$cwd" == "$dir" || "$cwd" == "$dir/"* ]]; then - error "Refusing to mount $cwd as workspace — system directory." + error "Refusing to mount $cwd as workspace — contains sensitive data." error "cd into a project subdirectory first." exit 1 fi From c3c3fcd0997c087d3f3972dc164bc0d625d08615 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 08:45:05 +0200 Subject: [PATCH 18/69] feat(workspace): add --kube flag to mount $HOME/.kube read-only into container --- claude.sh | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/claude.sh b/claude.sh index 14ba877..c9d3ff8 100644 --- a/claude.sh +++ b/claude.sh @@ -7,6 +7,9 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml" PROJECT="claude-secure" +# ─── Global flags ───────────────────────────────────────────────────────────── +ALLOW_KUBE=0 # set by --kube before the subcommand + # ─── Colours ────────────────────────────────────────────────────────────────── RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' info() { echo -e "${GREEN}[+]${NC} $*"; } @@ -98,6 +101,19 @@ workspace_flag() { echo "--volume ${cwd}:/workspace:z" } +# ─── Optional kubeconfig mount ──────────────────────────────────────────────── +# Enabled by passing --kube before the subcommand. +# Mounts $HOME/.kube read-only at /home/node/.kube inside the container. +kube_flag() { + [[ "$ALLOW_KUBE" -eq 0 ]] && return + local kube_dir="$HOME/.kube" + if [[ ! -d "$kube_dir" ]]; then + error "--kube specified but $kube_dir does not exist." + exit 1 + fi + echo "--volume ${kube_dir}:/home/node/.kube:ro,z" +} + # ─── Compose wrapper ────────────────────────────────────────────────────────── dc() { docker compose -f "$COMPOSE_FILE" -p "$PROJECT" "$@"; } @@ -114,7 +130,7 @@ cmd_start() { dc up -d proxy # no-op if already healthy; compose waits via depends_on info "Launching Claude Code..." # shellcheck disable=SC2046 - dc run --rm --service-ports $(workspace_flag) claude "$@" + dc run --rm --service-ports $(workspace_flag) $(kube_flag) claude "$@" } cmd_stop() { @@ -131,7 +147,7 @@ cmd_run() { dc up -d proxy info "Launching Claude Code..." # shellcheck disable=SC2046 - dc run --rm --service-ports $(workspace_flag) claude "$@" + dc run --rm --service-ports $(workspace_flag) $(kube_flag) claude "$@" } cmd_update() { @@ -157,7 +173,7 @@ cmd_shell() { load_env warn "Opening debug shell inside Claude container (non-Claude entrypoint)." # shellcheck disable=SC2046 - dc run --rm --service-ports --entrypoint /bin/bash $(workspace_flag) claude + dc run --rm --service-ports --entrypoint /bin/bash $(workspace_flag) $(kube_flag) claude } cmd_web() { @@ -206,8 +222,12 @@ Environment variables (set in .env or shell): WEBUI_USER Web interface username (default: claude). WEBUI_PASSWORD Required for web mode. Basic auth password. +Flags (before the subcommand): + --kube Mount \$HOME/.kube read-only at /home/node/.kube (kubectl access) + Examples: cd ~/myproject && ./claude.sh start + cd ~/myproject && ./claude.sh --kube start ./claude.sh web ./claude.sh logs proxy ./claude.sh logs webui @@ -216,6 +236,14 @@ EOF } # ─── Dispatch ───────────────────────────────────────────────────────────────── +# Parse global flags before the subcommand +while [[ "${1:-}" == --* ]]; do + case "$1" in + --kube) ALLOW_KUBE=1; shift ;; + *) break ;; + esac +done + case "${1:-help}" in start) shift; cmd_start "$@" ;; stop) cmd_stop ;; From 1c01d49f51b56706ee8562f2c1497d58e81fb8ee Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 08:47:32 +0200 Subject: [PATCH 19/69] feat(claude): install kubectl into container image --- claude/Dockerfile | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/claude/Dockerfile b/claude/Dockerfile index b48ae3a..a31c92b 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -8,6 +8,17 @@ RUN apk add --no-cache \ bash \ ttyd +# Install kubectl — architecture-aware, checksum-verified +RUN KUBECTL_VERSION=$(curl -fsSL https://dl.k8s.io/release/stable.txt) \ + && ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') \ + && curl -fsSL "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${ARCH}/kubectl" \ + -o /usr/local/bin/kubectl \ + && curl -fsSL "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${ARCH}/kubectl.sha256" \ + -o /tmp/kubectl.sha256 \ + && echo "$(cat /tmp/kubectl.sha256) /usr/local/bin/kubectl" | sha256sum -c \ + && rm /tmp/kubectl.sha256 \ + && chmod +x /usr/local/bin/kubectl + # Entrypoint used by the webui service (ttyd wrapping claude) COPY --chmod=755 webui-entrypoint.sh /usr/local/bin/webui-entrypoint.sh From 659fb3f3399a5ecc2b0f15657058657c0c870d37 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 08:49:11 +0200 Subject: [PATCH 20/69] feat(proxy): allow CONNECT tunnels to Kubernetes API server port 6443 --- proxy/squid.conf | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/proxy/squid.conf b/proxy/squid.conf index cac6ff8..55fba0d 100644 --- a/proxy/squid.conf +++ b/proxy/squid.conf @@ -18,10 +18,15 @@ coredump_dir /var/cache/squid # ─── ACL Definitions ────────────────────────────────────────────────────────── acl SSL_ports port 443 +acl SSL_ports port 6443 # Kubernetes API server acl Safe_ports port 80 acl Safe_ports port 443 +acl Safe_ports port 6443 # Kubernetes API server acl CONNECT method CONNECT +# Kubernetes API server — allow CONNECT tunnels to any cluster endpoint on :6443 +acl kubectl_api port 6443 + # ─── Egress allowlist ───────────────────────────────────────────────────────── # Add domains here as needed. Leading dot matches all subdomains. acl allowed_sites dstdomain api.anthropic.com @@ -43,6 +48,9 @@ http_access deny CONNECT !SSL_ports # Allow HTTPS tunnels only to allowlisted destinations http_access allow CONNECT allowed_sites +# Allow kubectl to reach any Kubernetes API server on the standard port +http_access allow CONNECT kubectl_api + # Allow plain HTTP only to allowlisted destinations http_access allow allowed_sites From 1dbbbc840dff5542dca122694a87fb90cd2fcfb7 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 08:56:25 +0200 Subject: [PATCH 21/69] ci: add Forgejo action to build and push Docker images to registry --- .forgejo/workflows/docker-build.yml | 45 +++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .forgejo/workflows/docker-build.yml diff --git a/.forgejo/workflows/docker-build.yml b/.forgejo/workflows/docker-build.yml new file mode 100644 index 0000000..038ffb8 --- /dev/null +++ b/.forgejo/workflows/docker-build.yml @@ -0,0 +1,45 @@ +name: Build and push Docker images + +on: + push: + branches: + - main + - master + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - service: claude + context: ./claude + - service: proxy + context: ./proxy + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to ${{ vars.REGISTRY_URL }} + uses: docker/login-action@v3 + with: + registry: ${{ vars.REGISTRY_URL }} + username: ${{ vars.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Build and push ${{ matrix.service }} + uses: docker/build-push-action@v5 + with: + context: ${{ matrix.context }} + push: true + tags: | + ${{ vars.REGISTRY_URL }}/${{ vars.REGISTRY_USER }}/${{ matrix.service }}:latest + ${{ vars.REGISTRY_URL }}/${{ vars.REGISTRY_USER }}/${{ matrix.service }}:${{ github.sha }} + cache-from: type=registry,ref=${{ vars.REGISTRY_URL }}/${{ vars.REGISTRY_USER }}/${{ matrix.service }}:cache + cache-to: type=registry,ref=${{ vars.REGISTRY_URL }}/${{ vars.REGISTRY_USER }}/${{ matrix.service }}:cache,mode=max From 50cfa9da4ead090c5a5eeb97cb79623d8134a4e1 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 16:49:55 +0200 Subject: [PATCH 22/69] fix workflow --- .forgejo/workflows/docker-build.yml | 77 ++++++++++++++++++----------- 1 file changed, 48 insertions(+), 29 deletions(-) diff --git a/.forgejo/workflows/docker-build.yml b/.forgejo/workflows/docker-build.yml index 038ffb8..09b6992 100644 --- a/.forgejo/workflows/docker-build.yml +++ b/.forgejo/workflows/docker-build.yml @@ -1,45 +1,64 @@ -name: Build and push Docker images +name: Build images on: push: branches: - main - - master - workflow_dispatch: +env: + # Set this to the public IP or hostname of your registry, + # whichever you use to reach it from your desktop/laptop + FORGEJO_HOST: code.zeidler.dev + HELM_EXPERIMENTAL_OCI: 1 jobs: - build: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - include: - - service: claude - context: ./claude - - service: proxy - context: ./proxy - + check-docker: + runs-on: docker-cli + services: + docker: + image: registry.zeidler.dev/docker-hub/catthehacker/ubuntu:act-latest + options: --privileged + container: + image: registry.zeidler.dev/docker-hub/catthehacker/ubuntu:act-latest steps: - - name: Checkout + - name: Wait for Docker daemon + run: | + timeout=300 # Set a timeout value in seconds + until docker info; do + echo "Waiting for Docker daemon to start..." + sleep 5 + timeout=$((timeout-5)) + if [ $timeout -le 0 ]; then + echo "Timeout waiting for Docker daemon to start." + exit 1 + fi + done + + build-and-push: + runs-on: docker-cli + services: + docker: + image: registry.zeidler.dev/docker-hub/catthehacker/ubuntu:act-latest + options: --privileged + environment: deploy + container: + image: registry.zeidler.dev/docker-hub/catthehacker/ubuntu:act-latest + steps: + - name: Checkout the repo uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to ${{ vars.REGISTRY_URL }} + - name: Login to the registry uses: docker/login-action@v3 with: registry: ${{ vars.REGISTRY_URL }} username: ${{ vars.REGISTRY_USER }} password: ${{ secrets.REGISTRY_PASSWORD }} - - - name: Build and push ${{ matrix.service }} - uses: docker/build-push-action@v5 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 with: - context: ${{ matrix.context }} + driver: docker-container + - name: Docker publish + uses: docker/build-push-action@v6 + with: + context: . push: true - tags: | - ${{ vars.REGISTRY_URL }}/${{ vars.REGISTRY_USER }}/${{ matrix.service }}:latest - ${{ vars.REGISTRY_URL }}/${{ vars.REGISTRY_USER }}/${{ matrix.service }}:${{ github.sha }} - cache-from: type=registry,ref=${{ vars.REGISTRY_URL }}/${{ vars.REGISTRY_USER }}/${{ matrix.service }}:cache - cache-to: type=registry,ref=${{ vars.REGISTRY_URL }}/${{ vars.REGISTRY_USER }}/${{ matrix.service }}:cache,mode=max + platforms: linux/amd64, linux/arm64 + tags: ${{ vars.REGISTRY_URL }}/docker/${{ env.GITHUB_REPOSITORY }}:0.1.${{ env.GITHUB_RUN_NUMBER }} From ff9ed447c0721e27950124e27a2a44b3d6c77500 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 16:52:40 +0200 Subject: [PATCH 23/69] update workflow --- .forgejo/workflows/docker-build.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.forgejo/workflows/docker-build.yml b/.forgejo/workflows/docker-build.yml index 09b6992..ea96e96 100644 --- a/.forgejo/workflows/docker-build.yml +++ b/.forgejo/workflows/docker-build.yml @@ -55,10 +55,17 @@ jobs: uses: docker/setup-buildx-action@v3 with: driver: docker-container - - name: Docker publish + - name: Docker publish proxy uses: docker/build-push-action@v6 with: - context: . + context: proxy push: true platforms: linux/amd64, linux/arm64 - tags: ${{ vars.REGISTRY_URL }}/docker/${{ env.GITHUB_REPOSITORY }}:0.1.${{ env.GITHUB_RUN_NUMBER }} + tags: ${{ vars.REGISTRY_URL }}/docker/${{ env.GITHUB_REPOSITORY }}-proxy:0.1.${{ env.GITHUB_RUN_NUMBER }} + - name: Docker publish claude + uses: docker/build-push-action@v6 + with: + context: claude + push: true + platforms: linux/amd64, linux/arm64 + tags: ${{ vars.REGISTRY_URL }}/docker/${{ env.GITHUB_REPOSITORY }}-claude:0.1.${{ env.GITHUB_RUN_NUMBER }} From 2d822305d13be7ac3a7c7d6abf100467ee4a5db4 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 17:02:43 +0200 Subject: [PATCH 24/69] refactor(images): pull from registry instead of building; add build.sh for local dev --- .env.example | 5 +++++ CLAUDE.md | 9 +++++---- README.md | 16 +++++++++++++--- build.sh | 17 +++++++++++++++++ claude.sh | 16 +++++----------- docker-compose.yml | 3 +++ 6 files changed, 48 insertions(+), 18 deletions(-) create mode 100755 build.sh diff --git a/.env.example b/.env.example index e8c6eea..9b353cd 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,11 @@ # Copy this file to .env and fill in your values. # .env is git-ignored — never commit it. +# ─── Image version ──────────────────────────────────────────────────────────── + +# Pin to a specific image tag. Defaults to "latest" if unset. +# IMAGE_TAG=0.1.42 + # ─── Authentication (choose one) ────────────────────────────────────────────── # Option 1: Anthropic API key diff --git a/CLAUDE.md b/CLAUDE.md index b9d8ea5..e65c01c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,11 +44,12 @@ docker-claude/ ## Development Workflow ```bash -chmod +x claude.sh +chmod +x claude.sh build.sh cp .env.example .env # set ANTHROPIC_API_KEY (and WEBUI_PASSWORD for web mode) -cd /path/to/project && ./claude.sh start # build + start proxy + launch Claude (mounts CWD as /workspace) -./claude.sh web # build + start proxy + webui (browser terminal on :7681) -./claude.sh update # rebuild images (no cache) after upstream updates +cd /path/to/project && ./claude.sh start # start proxy + launch Claude (pulls images, mounts CWD) +./claude.sh web # start proxy + webui (browser terminal on :7681) +./claude.sh update # pull latest images from registry +./build.sh # build images locally (development) ``` ## Coding Standards diff --git a/README.md b/README.md index 6f37c42..bdcab50 100644 --- a/README.md +++ b/README.md @@ -92,11 +92,12 @@ Then run `./claude.sh run` and follow the prompt. Credentials are stored in the ### CLI mode ```bash -# Build images, start proxy, launch Claude Code in the current directory +# 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 (faster on subsequent runs) +# Start proxy if needed, launch Claude Code ./claude.sh run ``` @@ -123,13 +124,22 @@ sbx ports --publish 7681:7681/tcp ```bash ./claude.sh stop # Stop and remove all containers -./claude.sh update # Rebuild images without cache +./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: + +```bash +./build.sh # build with layer cache +./build.sh --no-cache # force full rebuild +``` + ### Workspace | Mode | Workspace | diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..b761fdf --- /dev/null +++ b/build.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# build.sh — Build Docker images locally for development +# Usage: ./build.sh [--no-cache] [--push] +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml" +PROJECT="claude-secure" + +GREEN='\033[0;32m'; NC='\033[0m' +info() { echo -e "${GREEN}[+]${NC} $*"; } + +dc() { docker compose -f "$COMPOSE_FILE" -p "$PROJECT" "$@"; } + +info "Building images..." +dc build "$@" +info "Done. Run './claude.sh start' to launch." diff --git a/claude.sh b/claude.sh index c9d3ff8..9067b97 100644 --- a/claude.sh +++ b/claude.sh @@ -122,12 +122,8 @@ dc() { docker compose -f "$COMPOSE_FILE" -p "$PROJECT" "$@"; } cmd_start() { check_deps load_env - info "Building images..." - dc build info "Starting proxy sidecar..." dc up -d proxy - info "Waiting for proxy health check..." - dc up -d proxy # no-op if already healthy; compose waits via depends_on info "Launching Claude Code..." # shellcheck disable=SC2046 dc run --rm --service-ports $(workspace_flag) $(kube_flag) claude "$@" @@ -152,8 +148,8 @@ cmd_run() { cmd_update() { check_deps - info "Rebuilding images (no cache)..." - dc build --no-cache + info "Pulling latest images from registry..." + dc pull info "Update complete. Run './claude.sh start' to launch." } @@ -183,8 +179,6 @@ cmd_web() { error "WEBUI_PASSWORD is not set. Add it to .env before starting the web interface." exit 1 fi - info "Building images..." - dc build info "Starting proxy and web interface..." dc up -d webui local port=7681 @@ -206,12 +200,12 @@ cmd_help() { Usage: $(basename "$0") [args] Commands: - start [args] Build images, start proxy, launch Claude Code (CLI) + start [args] Start proxy, launch Claude Code (CLI) run [args] Start proxy if needed, launch Claude Code (CLI) - web Build images, start proxy + web interface (browser terminal) + web Start proxy + web interface (browser terminal) web-stop Stop the web interface (keeps proxy running) stop Stop and remove all containers - update Rebuild images without cache + update Pull latest images from the registry logs [svc] Tail logs (default: proxy) status Show container status shell Open a bash shell in the Claude container (debug) diff --git a/docker-compose.yml b/docker-compose.yml index e215390..c7d9b26 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,7 @@ services: # Bridges the isolated internal network to the internet. # Enforces an egress allowlist — see proxy/squid.conf. proxy: + image: registry.zeidler.dev/docker/playground/docker-claude-proxy:${IMAGE_TAG:-latest} build: context: proxy dockerfile: Dockerfile @@ -24,6 +25,7 @@ services: # No direct internet access. All egress routes through the proxy sidecar. # Run via "docker compose run --rm --service-ports claude" (managed by claude.sh). claude: + image: registry.zeidler.dev/docker/playground/docker-claude-claude:${IMAGE_TAG:-latest} build: context: claude/ dockerfile: Dockerfile @@ -64,6 +66,7 @@ services: # Protected by HTTP basic auth — set WEBUI_USER / WEBUI_PASSWORD in .env. # Network isolation is identical to the CLI container. webui: + image: registry.zeidler.dev/docker/playground/docker-claude-claude:${IMAGE_TAG:-latest} build: context: claude/ dockerfile: Dockerfile From f4a6bc0a998e28e034fd2ae1f6131759dff31144 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 17:05:44 +0200 Subject: [PATCH 25/69] fix(claude.sh): add --no-build to prevent fallback to local build --- claude.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/claude.sh b/claude.sh index 9067b97..4e38739 100644 --- a/claude.sh +++ b/claude.sh @@ -123,10 +123,10 @@ cmd_start() { check_deps load_env info "Starting proxy sidecar..." - dc up -d proxy + dc up -d --no-build proxy info "Launching Claude Code..." # shellcheck disable=SC2046 - dc run --rm --service-ports $(workspace_flag) $(kube_flag) claude "$@" + dc run --rm --no-build --service-ports $(workspace_flag) $(kube_flag) claude "$@" } cmd_stop() { @@ -140,10 +140,10 @@ cmd_run() { check_deps load_env info "Ensuring proxy is running..." - dc up -d proxy + dc up -d --no-build proxy info "Launching Claude Code..." # shellcheck disable=SC2046 - dc run --rm --service-ports $(workspace_flag) $(kube_flag) claude "$@" + dc run --rm --no-build --service-ports $(workspace_flag) $(kube_flag) claude "$@" } cmd_update() { @@ -169,7 +169,7 @@ cmd_shell() { load_env warn "Opening debug shell inside Claude container (non-Claude entrypoint)." # shellcheck disable=SC2046 - dc run --rm --service-ports --entrypoint /bin/bash $(workspace_flag) $(kube_flag) claude + dc run --rm --no-build --service-ports --entrypoint /bin/bash $(workspace_flag) $(kube_flag) claude } cmd_web() { @@ -180,7 +180,7 @@ cmd_web() { exit 1 fi info "Starting proxy and web interface..." - dc up -d webui + dc up -d --no-build webui local port=7681 info "Web interface is up → http://0.0.0.0:${port}" info "Credentials: ${WEBUI_USER:-claude} / [WEBUI_PASSWORD]" From a5af0a5427c92f7202d3685560e8b4395ea3e1c0 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 17:06:53 +0200 Subject: [PATCH 26/69] ci: also tag builds as latest --- .forgejo/workflows/docker-build.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/docker-build.yml b/.forgejo/workflows/docker-build.yml index ea96e96..55b7206 100644 --- a/.forgejo/workflows/docker-build.yml +++ b/.forgejo/workflows/docker-build.yml @@ -61,11 +61,15 @@ jobs: context: proxy push: true platforms: linux/amd64, linux/arm64 - tags: ${{ vars.REGISTRY_URL }}/docker/${{ env.GITHUB_REPOSITORY }}-proxy:0.1.${{ env.GITHUB_RUN_NUMBER }} + tags: | + ${{ vars.REGISTRY_URL }}/docker/${{ env.GITHUB_REPOSITORY }}-proxy:0.1.${{ env.GITHUB_RUN_NUMBER }} + ${{ vars.REGISTRY_URL }}/docker/${{ env.GITHUB_REPOSITORY }}-proxy:latest - name: Docker publish claude uses: docker/build-push-action@v6 with: context: claude push: true platforms: linux/amd64, linux/arm64 - tags: ${{ vars.REGISTRY_URL }}/docker/${{ env.GITHUB_REPOSITORY }}-claude:0.1.${{ env.GITHUB_RUN_NUMBER }} + tags: | + ${{ vars.REGISTRY_URL }}/docker/${{ env.GITHUB_REPOSITORY }}-claude:0.1.${{ env.GITHUB_RUN_NUMBER }} + ${{ vars.REGISTRY_URL }}/docker/${{ env.GITHUB_REPOSITORY }}-claude:latest From 3f91b27c9426afec0b9481540acdeab9822d28cd Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 17:14:37 +0200 Subject: [PATCH 27/69] refactor(claude.sh): use array for volume args, merge run into start, tighten helpers --- claude.sh | 188 ++++++++++++++++++------------------------------------ 1 file changed, 63 insertions(+), 125 deletions(-) mode change 100644 => 100755 claude.sh diff --git a/claude.sh b/claude.sh old mode 100644 new mode 100755 index 4e38739..0ca20a8 --- a/claude.sh +++ b/claude.sh @@ -1,14 +1,12 @@ #!/usr/bin/env bash # claude.sh — Manage the isolated Claude Code Docker environment -# Usage: ./claude.sh [args] +# Usage: ./claude.sh [--kube] [args] set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml" PROJECT="claude-secure" - -# ─── Global flags ───────────────────────────────────────────────────────────── -ALLOW_KUBE=0 # set by --kube before the subcommand +ALLOW_KUBE=0 # ─── Colours ────────────────────────────────────────────────────────────────── RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' @@ -16,19 +14,14 @@ info() { echo -e "${GREEN}[+]${NC} $*"; } warn() { echo -e "${YELLOW}[!]${NC} $*"; } error() { echo -e "${RED}[-]${NC} $*" >&2; } -# ─── Dependency check ───────────────────────────────────────────────────────── +# ─── Helpers ────────────────────────────────────────────────────────────────── check_deps() { - if ! command -v docker &>/dev/null; then - error "Docker is not installed. https://docs.docker.com/get-docker/" - exit 1 - fi - if ! docker compose version &>/dev/null 2>&1; then - error "Docker Compose v2 plugin is required." - exit 1 - fi + command -v docker &>/dev/null \ + || { error "Docker is not installed. https://docs.docker.com/get-docker/"; exit 1; } + docker compose version &>/dev/null 2>&1 \ + || { error "Docker Compose v2 plugin is required."; exit 1; } } -# ─── Environment loading ────────────────────────────────────────────────────── load_env() { local env_file="$SCRIPT_DIR/.env" if [[ -f "$env_file" ]]; then @@ -40,110 +33,67 @@ load_env() { warn "Claude Code will prompt you to authenticate on first run." warn " Option 1 (API key): set ANTHROPIC_API_KEY in .env" warn " Option 2 (token): run 'claude setup-token' and set CLAUDE_CODE_OAUTH_TOKEN in .env" - warn " Option 3 (browser): run './claude.sh run' and follow the login prompt;" - warn " port 54545 must be reachable from your browser." + warn " Option 3 (browser): log in when prompted; port 54545 must be reachable from your browser." fi } -# ─── Workspace volume resolution ────────────────────────────────────────────── -# Mounts the current working directory as /workspace inside the container. -# Refuses to mount home directories, key material, or system directories. -workspace_flag() { +dc() { docker compose -f "$COMPOSE_FILE" -p "$PROJECT" "$@"; } + +# ─── Volume args ────────────────────────────────────────────────────────────── +# Builds VOLUME_ARGS array for docker compose run. +# Validates the workspace path and optionally adds the kubeconfig mount. +build_volume_args() { local cwd cwd="$(pwd)" - # Exact-match blocklist — mounting these exposes too much of the host - local -a exact_blocked=( - / - "$HOME" - /root - /home - ) - - # Prefix blocklist — block these paths and all subdirectories. - # Covers system internals and credential/key material. - local -a prefix_blocked=( - /bin /sbin /lib /lib64 - /etc /usr /var - /proc /sys /dev - /boot /run - # SSH keys - "$HOME/.ssh" - /root/.ssh - # PGP/GPG keys - "$HOME/.gnupg" - /root/.gnupg - ) - + # Exact-match blocklist + local -a exact_blocked=( / "$HOME" /root /home ) for dir in "${exact_blocked[@]}"; do - if [[ "$cwd" == "$dir" ]]; then - error "Refusing to mount $cwd as workspace — too broad." - error "cd into a project subdirectory first." + [[ "$cwd" == "$dir" ]] && { + error "Refusing to mount $cwd as workspace — too broad. cd into a project subdirectory first." exit 1 - fi + } done - # Block any user home directory directly under /home (e.g. /home/alice) - if [[ "$cwd" =~ ^/home/[^/]+$ ]]; then - error "Refusing to mount $cwd as workspace — user home directory." - error "cd into a project subdirectory first." + # Any user home directory directly under /home + [[ "$cwd" =~ ^/home/[^/]+$ ]] && { + error "Refusing to mount $cwd as workspace — user home directory. cd into a project subdirectory first." exit 1 - fi + } + # Prefix blocklist — system internals and credential/key material + local -a prefix_blocked=( + /bin /sbin /lib /lib64 /etc /usr /var /proc /sys /dev /boot /run + "$HOME/.ssh" /root/.ssh "$HOME/.gnupg" /root/.gnupg + ) for dir in "${prefix_blocked[@]}"; do - if [[ "$cwd" == "$dir" || "$cwd" == "$dir/"* ]]; then - error "Refusing to mount $cwd as workspace — contains sensitive data." - error "cd into a project subdirectory first." + [[ "$cwd" == "$dir" || "$cwd" == "$dir/"* ]] && { + error "Refusing to mount $cwd as workspace — contains sensitive data. cd into a project subdirectory first." exit 1 - fi + } done - echo "--volume ${cwd}:/workspace:z" -} + VOLUME_ARGS=("--volume" "${cwd}:/workspace:z") -# ─── Optional kubeconfig mount ──────────────────────────────────────────────── -# Enabled by passing --kube before the subcommand. -# Mounts $HOME/.kube read-only at /home/node/.kube inside the container. -kube_flag() { - [[ "$ALLOW_KUBE" -eq 0 ]] && return - local kube_dir="$HOME/.kube" - if [[ ! -d "$kube_dir" ]]; then - error "--kube specified but $kube_dir does not exist." - exit 1 + if [[ "$ALLOW_KUBE" -eq 1 ]]; then + [[ -d "$HOME/.kube" ]] || { error "--kube: $HOME/.kube does not exist."; exit 1; } + VOLUME_ARGS+=("--volume" "$HOME/.kube:/home/node/.kube:ro,z") fi - echo "--volume ${kube_dir}:/home/node/.kube:ro,z" } -# ─── Compose wrapper ────────────────────────────────────────────────────────── -dc() { docker compose -f "$COMPOSE_FILE" -p "$PROJECT" "$@"; } - # ─── Commands ───────────────────────────────────────────────────────────────── - cmd_start() { - check_deps - load_env + check_deps; load_env; build_volume_args info "Starting proxy sidecar..." dc up -d --no-build proxy info "Launching Claude Code..." - # shellcheck disable=SC2046 - dc run --rm --no-build --service-ports $(workspace_flag) $(kube_flag) claude "$@" + dc run --rm --no-build --service-ports "${VOLUME_ARGS[@]}" claude "$@" } cmd_stop() { check_deps info "Stopping all containers..." dc down - info "Done." -} - -cmd_run() { - check_deps - load_env - info "Ensuring proxy is running..." - dc up -d --no-build proxy - info "Launching Claude Code..." - # shellcheck disable=SC2046 - dc run --rm --no-build --service-ports $(workspace_flag) $(kube_flag) claude "$@" } cmd_update() { @@ -155,8 +105,7 @@ cmd_update() { cmd_logs() { check_deps - local svc="${1:-proxy}" - dc logs -f "$svc" + dc logs -f "${1:-proxy}" } cmd_status() { @@ -165,44 +114,34 @@ cmd_status() { } cmd_shell() { - check_deps - load_env + check_deps; load_env; build_volume_args warn "Opening debug shell inside Claude container (non-Claude entrypoint)." - # shellcheck disable=SC2046 - dc run --rm --no-build --service-ports --entrypoint /bin/bash $(workspace_flag) $(kube_flag) claude + dc run --rm --no-build --service-ports --entrypoint /bin/bash "${VOLUME_ARGS[@]}" claude } cmd_web() { - check_deps - load_env - if [[ -z "${WEBUI_PASSWORD:-}" ]]; then - error "WEBUI_PASSWORD is not set. Add it to .env before starting the web interface." - exit 1 - fi + check_deps; load_env + [[ -n "${WEBUI_PASSWORD:-}" ]] \ + || { error "WEBUI_PASSWORD is not set. Add it to .env before starting the web interface."; exit 1; } info "Starting proxy and web interface..." dc up -d --no-build webui - local port=7681 - info "Web interface is up → http://0.0.0.0:${port}" + info "Web interface is up → http://0.0.0.0:7681" info "Credentials: ${WEBUI_USER:-claude} / [WEBUI_PASSWORD]" - warn "To reach it from outside this host, publish the port:" - warn " sbx ports --publish ${port}:${port}/tcp" } cmd_web_stop() { check_deps info "Stopping web interface..." - dc stop webui - dc rm -f webui + dc stop webui && dc rm -f webui } cmd_help() { cat < [args] +Usage: $(basename "$0") [--kube] [args] Commands: start [args] Start proxy, launch Claude Code (CLI) - run [args] Start proxy if needed, launch Claude Code (CLI) - web Start proxy + web interface (browser terminal) + web Start proxy + web interface (browser terminal on :7681) web-stop Stop the web interface (keeps proxy running) stop Stop and remove all containers update Pull latest images from the registry @@ -211,26 +150,26 @@ Commands: shell Open a bash shell in the Claude container (debug) help Show this message -Environment variables (set in .env or shell): - ANTHROPIC_API_KEY Required for all modes. - WEBUI_USER Web interface username (default: claude). - WEBUI_PASSWORD Required for web mode. Basic auth password. - Flags (before the subcommand): --kube Mount \$HOME/.kube read-only at /home/node/.kube (kubectl access) +Environment variables (set in .env): + ANTHROPIC_API_KEY API key auth + CLAUDE_CODE_OAUTH_TOKEN OAuth token auth (from 'claude setup-token') + IMAGE_TAG Image tag to use (default: latest) + WEBUI_USER Web interface username (default: claude) + WEBUI_PASSWORD Required for web mode + Examples: cd ~/myproject && ./claude.sh start cd ~/myproject && ./claude.sh --kube start ./claude.sh web ./claude.sh logs proxy - ./claude.sh logs webui ./claude.sh shell EOF } # ─── Dispatch ───────────────────────────────────────────────────────────────── -# Parse global flags before the subcommand while [[ "${1:-}" == --* ]]; do case "$1" in --kube) ALLOW_KUBE=1; shift ;; @@ -239,16 +178,15 @@ while [[ "${1:-}" == --* ]]; do done case "${1:-help}" in - start) shift; cmd_start "$@" ;; - stop) cmd_stop ;; - run) shift; cmd_run "$@" ;; - web) cmd_web ;; - web-stop) cmd_web_stop ;; - update) cmd_update ;; - logs) shift; cmd_logs "${1:-}" ;; - status) cmd_status ;; - shell) cmd_shell ;; - help|-h|--help) cmd_help ;; + start|run) shift; cmd_start "$@" ;; + stop) cmd_stop ;; + web) cmd_web ;; + web-stop) cmd_web_stop ;; + update) cmd_update ;; + logs) shift; cmd_logs "${1:-}" ;; + status) cmd_status ;; + shell) cmd_shell ;; + help|-h|--help) cmd_help ;; *) error "Unknown command: ${1}" cmd_help From 6a060aa8ab02ca17579eb2685de919a2852c42b1 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 17:15:51 +0200 Subject: [PATCH 28/69] fix(workflow): remove build contexts from compose; build.sh uses docker build directly --- build.sh | 14 ++++++++------ claude.sh | 8 ++++---- docker-compose.yml | 9 --------- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/build.sh b/build.sh index b761fdf..75127ba 100755 --- a/build.sh +++ b/build.sh @@ -1,17 +1,19 @@ #!/usr/bin/env bash # build.sh — Build Docker images locally for development -# Usage: ./build.sh [--no-cache] [--push] +# Usage: ./build.sh [docker build flags, e.g. --no-cache] set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml" -PROJECT="claude-secure" +REGISTRY="registry.zeidler.dev/docker/playground" +TAG="${IMAGE_TAG:-latest}" GREEN='\033[0;32m'; NC='\033[0m' info() { echo -e "${GREEN}[+]${NC} $*"; } -dc() { docker compose -f "$COMPOSE_FILE" -p "$PROJECT" "$@"; } +info "Building proxy..." +docker build "$@" -t "${REGISTRY}/docker-claude-proxy:${TAG}" "${SCRIPT_DIR}/proxy" + +info "Building claude..." +docker build "$@" -t "${REGISTRY}/docker-claude-claude:${TAG}" "${SCRIPT_DIR}/claude" -info "Building images..." -dc build "$@" info "Done. Run './claude.sh start' to launch." diff --git a/claude.sh b/claude.sh index 0ca20a8..89f355a 100755 --- a/claude.sh +++ b/claude.sh @@ -85,9 +85,9 @@ build_volume_args() { cmd_start() { check_deps; load_env; build_volume_args info "Starting proxy sidecar..." - dc up -d --no-build proxy + dc up -d proxy info "Launching Claude Code..." - dc run --rm --no-build --service-ports "${VOLUME_ARGS[@]}" claude "$@" + dc run --rm --service-ports "${VOLUME_ARGS[@]}" claude "$@" } cmd_stop() { @@ -116,7 +116,7 @@ cmd_status() { cmd_shell() { check_deps; load_env; build_volume_args warn "Opening debug shell inside Claude container (non-Claude entrypoint)." - dc run --rm --no-build --service-ports --entrypoint /bin/bash "${VOLUME_ARGS[@]}" claude + dc run --rm --service-ports --entrypoint /bin/bash "${VOLUME_ARGS[@]}" claude } cmd_web() { @@ -124,7 +124,7 @@ cmd_web() { [[ -n "${WEBUI_PASSWORD:-}" ]] \ || { error "WEBUI_PASSWORD is not set. Add it to .env before starting the web interface."; exit 1; } info "Starting proxy and web interface..." - dc up -d --no-build webui + dc up -d webui info "Web interface is up → http://0.0.0.0:7681" info "Credentials: ${WEBUI_USER:-claude} / [WEBUI_PASSWORD]" } diff --git a/docker-compose.yml b/docker-compose.yml index c7d9b26..6242267 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,9 +4,6 @@ services: # Enforces an egress allowlist — see proxy/squid.conf. proxy: image: registry.zeidler.dev/docker/playground/docker-claude-proxy:${IMAGE_TAG:-latest} - build: - context: proxy - dockerfile: Dockerfile networks: - claude-internal # reachable by claude and webui containers - proxy-external # has outbound internet access @@ -26,9 +23,6 @@ services: # Run via "docker compose run --rm --service-ports claude" (managed by claude.sh). claude: image: registry.zeidler.dev/docker/playground/docker-claude-claude:${IMAGE_TAG:-latest} - build: - context: claude/ - dockerfile: Dockerfile depends_on: proxy: condition: service_healthy @@ -67,9 +61,6 @@ services: # Network isolation is identical to the CLI container. webui: image: registry.zeidler.dev/docker/playground/docker-claude-claude:${IMAGE_TAG:-latest} - build: - context: claude/ - dockerfile: Dockerfile entrypoint: ["/usr/local/bin/webui-entrypoint.sh"] depends_on: proxy: From f07a30bd0b9604d29aea68c3df0150761d5692a1 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 17:16:58 +0200 Subject: [PATCH 29/69] chore(hooks): enforce executable bit on claude.sh and build.sh via pre-commit hook --- CLAUDE.md | 8 ++++++++ hooks/pre-commit | 13 +++++++++++++ 2 files changed, 21 insertions(+) create mode 100755 hooks/pre-commit diff --git a/CLAUDE.md b/CLAUDE.md index e65c01c..876db89 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,6 +52,14 @@ cd /path/to/project && ./claude.sh start # start proxy + launch Claude (pulls i ./build.sh # build images locally (development) ``` +## Git Hooks + +A pre-commit hook lives in `hooks/` and enforces the executable bit on `claude.sh` and `build.sh`. Activate it once after cloning: + +```bash +git config core.hooksPath hooks +``` + ## Coding Standards - Shell scripts use `set -euo pipefail` diff --git a/hooks/pre-commit b/hooks/pre-commit new file mode 100755 index 0000000..113d4ad --- /dev/null +++ b/hooks/pre-commit @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Ensure control scripts stay executable. +set -euo pipefail + +SCRIPTS=(claude.sh build.sh) + +for f in "${SCRIPTS[@]}"; do + if [[ -f "$f" && ! -x "$f" ]]; then + echo "pre-commit: fixing missing executable bit on $f" + chmod +x "$f" + git add "$f" + fi +done From c32842751770852b2ac717c86cfce3fea4873fc3 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 17:53:26 +0200 Subject: [PATCH 30/69] feat(proxy): allow platform.claude.com in egress allowlist --- proxy/squid.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/proxy/squid.conf b/proxy/squid.conf index 55fba0d..0cf94da 100644 --- a/proxy/squid.conf +++ b/proxy/squid.conf @@ -31,6 +31,7 @@ acl kubectl_api port 6443 # Add domains here as needed. Leading dot matches all subdomains. acl allowed_sites dstdomain api.anthropic.com acl allowed_sites dstdomain statsig.anthropic.com +acl allowed_sites dstdomain platform.claude.com acl allowed_sites dstdomain localhost acl allowed_sites dstdomain .local # MCP servers From ab7e909c3c8fa79f1e80c5a37075fc7d50ded871 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 17:55:01 +0200 Subject: [PATCH 31/69] make sure that files are executable --- build.sh | 0 hooks/pre-commit | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 build.sh mode change 100755 => 100644 hooks/pre-commit diff --git a/build.sh b/build.sh old mode 100755 new mode 100644 diff --git a/hooks/pre-commit b/hooks/pre-commit old mode 100755 new mode 100644 From 1b141b200c99e152bd8e6c0744c20d9e9278ac4b Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 19:18:39 +0200 Subject: [PATCH 32/69] use new native install --- build.sh | 0 claude/Dockerfile | 39 ++++++++++++++++++++------------------- hooks/pre-commit | 0 3 files changed, 20 insertions(+), 19 deletions(-) mode change 100644 => 100755 build.sh mode change 100644 => 100755 hooks/pre-commit diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 diff --git a/claude/Dockerfile b/claude/Dockerfile index a31c92b..9c6e086 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -2,22 +2,22 @@ FROM node:20-alpine # Install runtime dependencies RUN apk add --no-cache \ - git \ - curl \ - ca-certificates \ - bash \ - ttyd + git \ + curl \ + ca-certificates \ + bash \ + ttyd # Install kubectl — architecture-aware, checksum-verified RUN KUBECTL_VERSION=$(curl -fsSL https://dl.k8s.io/release/stable.txt) \ - && ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') \ - && curl -fsSL "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${ARCH}/kubectl" \ - -o /usr/local/bin/kubectl \ - && curl -fsSL "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${ARCH}/kubectl.sha256" \ - -o /tmp/kubectl.sha256 \ - && echo "$(cat /tmp/kubectl.sha256) /usr/local/bin/kubectl" | sha256sum -c \ - && rm /tmp/kubectl.sha256 \ - && chmod +x /usr/local/bin/kubectl + && ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') \ + && curl -fsSL "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${ARCH}/kubectl" \ + -o /usr/local/bin/kubectl \ + && curl -fsSL "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${ARCH}/kubectl.sha256" \ + -o /tmp/kubectl.sha256 \ + && echo "$(cat /tmp/kubectl.sha256) /usr/local/bin/kubectl" | sha256sum -c \ + && rm /tmp/kubectl.sha256 \ + && chmod +x /usr/local/bin/kubectl # Entrypoint used by the webui service (ttyd wrapping claude) COPY --chmod=755 webui-entrypoint.sh /usr/local/bin/webui-entrypoint.sh @@ -27,21 +27,22 @@ COPY --chmod=755 webui-entrypoint.sh /usr/local/bin/webui-entrypoint.sh COPY settings.json /etc/claude-code/managed-settings.json # Install Claude Code globally -RUN npm install -g @anthropic-ai/claude-code +RUN curl -fsSL https://claude.ai/install.sh | bash + # Install MCP servers globally — entry points land in /usr/local/lib/node_modules/ RUN npm install -g \ - @modelcontextprotocol/server-github \ - @yoda.digital/gitlab-mcp-server \ - @aashari/mcp-server-atlassian-jira \ - @aashari/mcp-server-atlassian-confluence + @modelcontextprotocol/server-github \ + @yoda.digital/gitlab-mcp-server \ + @aashari/mcp-server-atlassian-jira \ + @aashari/mcp-server-atlassian-confluence # Workspace and Claude config dir — owned by the built-in node user (uid 1000). # Pre-creating ~/.claude ensures the named volume is initialised with the # correct ownership when first mounted (Docker copies image content into # an empty named volume on first use). RUN mkdir -p /workspace /home/node/.claude \ - && chown -R node:node /workspace /home/node/.claude + && chown -R node:node /workspace /home/node/.claude USER node WORKDIR /workspace diff --git a/hooks/pre-commit b/hooks/pre-commit old mode 100644 new mode 100755 From 4edef5ac1a205e4d641a59ad1a9a3da24de972b6 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 19:18:48 +0200 Subject: [PATCH 33/69] fix stuff finally? --- build.sh | 0 claude.sh | 4 ++++ hooks/pre-commit | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) mode change 100755 => 100644 build.sh mode change 100755 => 100644 hooks/pre-commit diff --git a/build.sh b/build.sh old mode 100755 new mode 100644 diff --git a/claude.sh b/claude.sh index 89f355a..62dcaa6 100755 --- a/claude.sh +++ b/claude.sh @@ -84,6 +84,8 @@ build_volume_args() { # ─── Commands ───────────────────────────────────────────────────────────────── cmd_start() { check_deps; load_env; build_volume_args + info "Pulling latest images..." + dc pull info "Starting proxy sidecar..." dc up -d proxy info "Launching Claude Code..." @@ -123,6 +125,8 @@ cmd_web() { check_deps; load_env [[ -n "${WEBUI_PASSWORD:-}" ]] \ || { error "WEBUI_PASSWORD is not set. Add it to .env before starting the web interface."; exit 1; } + info "Pulling latest images..." + dc pull info "Starting proxy and web interface..." dc up -d webui info "Web interface is up → http://0.0.0.0:7681" diff --git a/hooks/pre-commit b/hooks/pre-commit old mode 100755 new mode 100644 index 113d4ad..9e6e3ae --- a/hooks/pre-commit +++ b/hooks/pre-commit @@ -2,7 +2,7 @@ # Ensure control scripts stay executable. set -euo pipefail -SCRIPTS=(claude.sh build.sh) +SCRIPTS=(claude.sh build.sh hooks/pre-commit) for f in "${SCRIPTS[@]}"; do if [[ -f "$f" && ! -x "$f" ]]; then From 2002ea7b32861afe9b4668f0f0b9eb23fdc115f1 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 21:19:33 +0200 Subject: [PATCH 34/69] chore(registry): use docker-public registry path Update image references from registry.zeidler.dev/docker/playground to registry.zeidler.dev/docker-public/playground in docker-compose.yml and build.sh. Also bind-mount ${HOME}/.claude instead of using the claude-config named volume. Co-Authored-By: Claude Sonnet 4.6 --- build.sh | 2 +- docker-compose.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build.sh b/build.sh index 75127ba..fe5f818 100644 --- a/build.sh +++ b/build.sh @@ -4,7 +4,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REGISTRY="registry.zeidler.dev/docker/playground" +REGISTRY="registry.zeidler.dev/docker-public/playground" TAG="${IMAGE_TAG:-latest}" GREEN='\033[0;32m'; NC='\033[0m' diff --git a/docker-compose.yml b/docker-compose.yml index 6242267..411b00e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ services: # Bridges the isolated internal network to the internet. # Enforces an egress allowlist — see proxy/squid.conf. proxy: - image: registry.zeidler.dev/docker/playground/docker-claude-proxy:${IMAGE_TAG:-latest} + image: registry.zeidler.dev/docker-public/playground/docker-claude-proxy:${IMAGE_TAG:-latest} networks: - claude-internal # reachable by claude and webui containers - proxy-external # has outbound internet access @@ -22,7 +22,7 @@ services: # No direct internet access. All egress routes through the proxy sidecar. # Run via "docker compose run --rm --service-ports claude" (managed by claude.sh). claude: - image: registry.zeidler.dev/docker/playground/docker-claude-claude:${IMAGE_TAG:-latest} + image: registry.zeidler.dev/docker-public/playground/docker-claude-claude:${IMAGE_TAG:-latest} depends_on: proxy: condition: service_healthy @@ -46,7 +46,7 @@ services: # OAuth callback — required for browser-based login (claude login) - "0.0.0.0:54545:54545" volumes: - - claude-config:/home/node/.claude + - ${HOME}/.claude:/home/node/.claude # Workspace is injected by claude.sh via --volume flag at run time (current directory). security_opt: - no-new-privileges:true @@ -60,7 +60,7 @@ services: # Protected by HTTP basic auth — set WEBUI_USER / WEBUI_PASSWORD in .env. # Network isolation is identical to the CLI container. webui: - image: registry.zeidler.dev/docker/playground/docker-claude-claude:${IMAGE_TAG:-latest} + image: registry.zeidler.dev/docker-public/playground/docker-claude-claude:${IMAGE_TAG:-latest} entrypoint: ["/usr/local/bin/webui-entrypoint.sh"] depends_on: proxy: From 8b4f08e68c5fac5524b69474ce3efeda035a00d0 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 21:19:46 +0200 Subject: [PATCH 35/69] chore(hooks): fix executable bit on build.sh and hooks/pre-commit Co-Authored-By: Claude Sonnet 4.6 --- build.sh | 0 hooks/pre-commit | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 build.sh mode change 100644 => 100755 hooks/pre-commit diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 diff --git a/hooks/pre-commit b/hooks/pre-commit old mode 100644 new mode 100755 From 1dee611fb36fcbc99cb586bff2d90c20378c2b07 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 21:39:10 +0200 Subject: [PATCH 36/69] fix repository path --- .forgejo/workflows/docker-build.yml | 8 ++++---- hooks/pre-commit | 0 2 files changed, 4 insertions(+), 4 deletions(-) mode change 100755 => 100644 hooks/pre-commit diff --git a/.forgejo/workflows/docker-build.yml b/.forgejo/workflows/docker-build.yml index 55b7206..3f76b5d 100644 --- a/.forgejo/workflows/docker-build.yml +++ b/.forgejo/workflows/docker-build.yml @@ -62,8 +62,8 @@ jobs: push: true platforms: linux/amd64, linux/arm64 tags: | - ${{ vars.REGISTRY_URL }}/docker/${{ env.GITHUB_REPOSITORY }}-proxy:0.1.${{ env.GITHUB_RUN_NUMBER }} - ${{ vars.REGISTRY_URL }}/docker/${{ env.GITHUB_REPOSITORY }}-proxy:latest + ${{ vars.REGISTRY_URL }}/docker-public/${{ env.GITHUB_REPOSITORY }}-proxy:0.1.${{ env.GITHUB_RUN_NUMBER }} + ${{ vars.REGISTRY_URL }}/docker-public/${{ env.GITHUB_REPOSITORY }}-proxy:latest - name: Docker publish claude uses: docker/build-push-action@v6 with: @@ -71,5 +71,5 @@ jobs: push: true platforms: linux/amd64, linux/arm64 tags: | - ${{ vars.REGISTRY_URL }}/docker/${{ env.GITHUB_REPOSITORY }}-claude:0.1.${{ env.GITHUB_RUN_NUMBER }} - ${{ vars.REGISTRY_URL }}/docker/${{ env.GITHUB_REPOSITORY }}-claude:latest + ${{ vars.REGISTRY_URL }}/docker-public/${{ env.GITHUB_REPOSITORY }}-claude:0.1.${{ env.GITHUB_RUN_NUMBER }} + ${{ vars.REGISTRY_URL }}/docker-public/${{ env.GITHUB_REPOSITORY }}-claude:latest diff --git a/hooks/pre-commit b/hooks/pre-commit old mode 100755 new mode 100644 From e78a302cb9f705946f1def5f29f0521148fec15e Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 21:59:08 +0200 Subject: [PATCH 37/69] feat: remove webui --- .env.example | 6 ----- CLAUDE.md | 13 +++------- claude.sh | 30 +++------------------- claude/Dockerfile | 6 +---- claude/webui-entrypoint.sh | 14 ---------- docker-compose.yml | 52 +------------------------------------- 6 files changed, 9 insertions(+), 112 deletions(-) delete mode 100644 claude/webui-entrypoint.sh diff --git a/.env.example b/.env.example index 9b353cd..ad79c7b 100644 --- a/.env.example +++ b/.env.example @@ -19,12 +19,6 @@ # Port 54545 must be reachable from your browser for the OAuth callback. # Run: sbx ports --publish 54545:54545/tcp -# ─── Web interface ──────────────────────────────────────────────────────────── - -# Required for ./claude.sh web -# WEBUI_USER=claude -# WEBUI_PASSWORD=changeme - # ─── MCP servers (all optional) ─────────────────────────────────────────────── # GitHub — PAT with repo scope diff --git a/CLAUDE.md b/CLAUDE.md index 876db89..687a431 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,16 +8,13 @@ This file provides context and guidance for working with this project. ## Architecture -Three containers managed by Docker Compose: +Two containers managed by Docker Compose: - **`claude`** — Claude Code CLI (`node:20-alpine`), runs as the built-in `node` user (UID 1000), isolated to an internal-only Docker network -- **`webui`** — Claude Code as a browser terminal via ttyd (`node:20-alpine`), `node` user (UID 1000), same network isolation, basic auth required - **`proxy`** — Squid forward proxy (`alpine:3.21`), `squid` user, bridges the internal network to the internet with an egress allowlist Key Docker network property: `claude-internal` has `internal: true`, meaning Docker adds no default gateway. The `claude` and `webui` containers physically cannot reach the internet without going through the `proxy` container. -The `webui` service reuses `claude/Dockerfile`. Its entrypoint (`claude/webui-entrypoint.sh`) starts `ttyd --credential user:pass claude` instead of `claude` directly. - Auth supports three modes (checked at startup by `claude.sh`): - `ANTHROPIC_API_KEY` — API key - `CLAUDE_CODE_OAUTH_TOKEN` — 1-year token from `claude setup-token` (headless-friendly) @@ -27,11 +24,10 @@ Auth supports three modes (checked at startup by `claude.sh`): ``` docker-claude/ -├── claude.sh # Control script: start/stop/run/web/web-stop/update/logs/status/shell +├── claude.sh # Control script: start/stop/run/update/logs/status/shell ├── docker-compose.yml # Service definitions and network topology ├── claude/ -│ ├── Dockerfile # Claude Code + ttyd (node:20-alpine, UID 1000) -│ └── webui-entrypoint.sh # Entrypoint for webui: starts ttyd wrapping claude +│ └── Dockerfile # Claude Code (node:20-alpine, UID 1000) ├── proxy/ │ ├── Dockerfile # Squid proxy sidecar (alpine:3.21, squid user) │ └── squid.conf # Squid ACL config — egress allowlist lives here @@ -45,9 +41,8 @@ docker-claude/ ```bash chmod +x claude.sh build.sh -cp .env.example .env # set ANTHROPIC_API_KEY (and WEBUI_PASSWORD for web mode) +cp .env.example .env # set ANTHROPIC_API_KEY cd /path/to/project && ./claude.sh start # start proxy + launch Claude (pulls images, mounts CWD) -./claude.sh web # start proxy + webui (browser terminal on :7681) ./claude.sh update # pull latest images from registry ./build.sh # build images locally (development) ``` diff --git a/claude.sh b/claude.sh index 62dcaa6..a78dc25 100755 --- a/claude.sh +++ b/claude.sh @@ -37,6 +37,7 @@ load_env() { fi } +# Wrapper so every docker compose call uses the right file and project name. dc() { docker compose -f "$COMPOSE_FILE" -p "$PROJECT" "$@"; } # ─── Volume args ────────────────────────────────────────────────────────────── @@ -121,32 +122,12 @@ cmd_shell() { dc run --rm --service-ports --entrypoint /bin/bash "${VOLUME_ARGS[@]}" claude } -cmd_web() { - check_deps; load_env - [[ -n "${WEBUI_PASSWORD:-}" ]] \ - || { error "WEBUI_PASSWORD is not set. Add it to .env before starting the web interface."; exit 1; } - info "Pulling latest images..." - dc pull - info "Starting proxy and web interface..." - dc up -d webui - info "Web interface is up → http://0.0.0.0:7681" - info "Credentials: ${WEBUI_USER:-claude} / [WEBUI_PASSWORD]" -} - -cmd_web_stop() { - check_deps - info "Stopping web interface..." - dc stop webui && dc rm -f webui -} - cmd_help() { cat < [args] Commands: start [args] Start proxy, launch Claude Code (CLI) - web Start proxy + web interface (browser terminal on :7681) - web-stop Stop the web interface (keeps proxy running) stop Stop and remove all containers update Pull latest images from the registry logs [svc] Tail logs (default: proxy) @@ -158,16 +139,13 @@ Flags (before the subcommand): --kube Mount \$HOME/.kube read-only at /home/node/.kube (kubectl access) Environment variables (set in .env): - ANTHROPIC_API_KEY API key auth + ANTHROPIC_API_KEY API key auth CLAUDE_CODE_OAUTH_TOKEN OAuth token auth (from 'claude setup-token') - IMAGE_TAG Image tag to use (default: latest) - WEBUI_USER Web interface username (default: claude) - WEBUI_PASSWORD Required for web mode + IMAGE_TAG Image tag to use (default: latest) Examples: cd ~/myproject && ./claude.sh start cd ~/myproject && ./claude.sh --kube start - ./claude.sh web ./claude.sh logs proxy ./claude.sh shell EOF @@ -184,8 +162,6 @@ done case "${1:-help}" in start|run) shift; cmd_start "$@" ;; stop) cmd_stop ;; - web) cmd_web ;; - web-stop) cmd_web_stop ;; update) cmd_update ;; logs) shift; cmd_logs "${1:-}" ;; status) cmd_status ;; diff --git a/claude/Dockerfile b/claude/Dockerfile index 9c6e086..ccede4c 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -5,8 +5,7 @@ RUN apk add --no-cache \ git \ curl \ ca-certificates \ - bash \ - ttyd + bash # Install kubectl — architecture-aware, checksum-verified RUN KUBECTL_VERSION=$(curl -fsSL https://dl.k8s.io/release/stable.txt) \ @@ -19,9 +18,6 @@ RUN KUBECTL_VERSION=$(curl -fsSL https://dl.k8s.io/release/stable.txt) \ && rm /tmp/kubectl.sha256 \ && chmod +x /usr/local/bin/kubectl -# Entrypoint used by the webui service (ttyd wrapping claude) -COPY --chmod=755 webui-entrypoint.sh /usr/local/bin/webui-entrypoint.sh - # System-level Claude Code policy — owned by root, not writable by the node user. # Restricts available models; cannot be bypassed via CLI flags or env vars. COPY settings.json /etc/claude-code/managed-settings.json diff --git a/claude/webui-entrypoint.sh b/claude/webui-entrypoint.sh deleted file mode 100644 index 4cf31cd..0000000 --- a/claude/webui-entrypoint.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -# Entrypoint for the webui service. -# Wraps Claude Code in ttyd (terminal-over-WebSocket) with basic auth. -set -euo pipefail - -: "${WEBUI_PASSWORD:?WEBUI_PASSWORD must be set in .env}" -WEBUI_USER="${WEBUI_USER:-claude}" -WEBUI_PORT="${WEBUI_PORT:-7681}" - -exec ttyd \ - --port "${WEBUI_PORT}" \ - --writable \ - --credential "${WEBUI_USER}:${WEBUI_PASSWORD}" \ - claude diff --git a/docker-compose.yml b/docker-compose.yml index 411b00e..4148250 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: proxy: image: registry.zeidler.dev/docker-public/playground/docker-claude-proxy:${IMAGE_TAG:-latest} networks: - - claude-internal # reachable by claude and webui containers + - claude-internal # reachable by claude container - proxy-external # has outbound internet access restart: unless-stopped security_opt: @@ -55,50 +55,6 @@ services: stdin_open: true tty: true - # ─── Claude Code web interface ───────────────────────────────────────────── - # Serves Claude Code as a browser terminal via ttyd (port 7681). - # Protected by HTTP basic auth — set WEBUI_USER / WEBUI_PASSWORD in .env. - # Network isolation is identical to the CLI container. - webui: - image: registry.zeidler.dev/docker-public/playground/docker-claude-claude:${IMAGE_TAG:-latest} - entrypoint: ["/usr/local/bin/webui-entrypoint.sh"] - depends_on: - proxy: - condition: service_healthy - networks: - - claude-internal # only — no route to the internet - environment: - - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} - - CLAUDE_CODE_OAUTH_TOKEN=${CLAUDE_CODE_OAUTH_TOKEN:-} - - HTTP_PROXY=http://proxy:3128 - - HTTPS_PROXY=http://proxy:3128 - - ALL_PROXY=http://proxy:3128 - - NO_PROXY=localhost,127.0.0.1 - # MCP server credentials — all optional; servers are skipped if unset - - GITHUB_TOKEN=${GITHUB_TOKEN:-} - - GITLAB_TOKEN=${GITLAB_TOKEN:-} - - GITLAB_URL=${GITLAB_URL:-https://gitlab.com} - - ATLASSIAN_SITE_NAME=${ATLASSIAN_SITE_NAME:-} - - ATLASSIAN_USER_EMAIL=${ATLASSIAN_USER_EMAIL:-} - - ATLASSIAN_API_TOKEN=${ATLASSIAN_API_TOKEN:-} - - WEBUI_USER=${WEBUI_USER:-claude} - - WEBUI_PASSWORD=${WEBUI_PASSWORD:-} - - WEBUI_PORT=7681 - ports: - - "0.0.0.0:7681:7681" - # OAuth callback — required for browser-based login (claude login) - - "0.0.0.0:54545:54545" - volumes: - - claude-config:/home/node/.claude - - claude-web-workspace:/workspace - security_opt: - - no-new-privileges:true - cap_drop: - - ALL - stdin_open: true - tty: true - restart: unless-stopped - networks: # Internal-only: Docker adds no default gateway → no direct internet route claude-internal: @@ -109,9 +65,3 @@ networks: proxy-external: driver: bridge -volumes: - # Persists Claude Code auth credentials (~/.claude/) across container runs. - # Shared between the CLI and web interface so login carries over. - claude-config: - # Persistent workspace for the web interface - claude-web-workspace: From 27feedf65ebfa03deb33b78569af65204e5756f9 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 22:02:15 +0200 Subject: [PATCH 38/69] chore(hooks): restore executable bit on hooks/pre-commit Co-Authored-By: Claude Sonnet 4.6 --- hooks/pre-commit | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 hooks/pre-commit diff --git a/hooks/pre-commit b/hooks/pre-commit old mode 100644 new mode 100755 From b76d1e5e2a1b85a5a2be5da49fdc3e3a1231d30a Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 22:40:01 +0200 Subject: [PATCH 39/69] chore(docker): pin Claude Code install to stable release channel Co-Authored-By: Claude Sonnet 4.6 --- claude/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/claude/Dockerfile b/claude/Dockerfile index ccede4c..d08c1cf 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -23,7 +23,7 @@ RUN KUBECTL_VERSION=$(curl -fsSL https://dl.k8s.io/release/stable.txt) \ COPY settings.json /etc/claude-code/managed-settings.json # Install Claude Code globally -RUN curl -fsSL https://claude.ai/install.sh | bash +RUN curl -fsSL https://claude.ai/install.sh | ash -s stable # Install MCP servers globally — entry points land in /usr/local/lib/node_modules/ From f4cf8056e952f37a9443ce42eb25af82947f5f38 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 22:41:28 +0200 Subject: [PATCH 40/69] docs(readme): sync with current state after webui removal Remove webui from architecture, commands, and security table. Update auth option 3 to reference ~/.claude instead of claude-config volume. Drop stale registry path comment and web interface section. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 66 ++++++++++++------------------------------------------- 1 file changed, 14 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index bdcab50..f87d6d2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # docker-claude -Runs [Claude Code](https://claude.ai/code) inside an isolated Docker environment with a proxy sidecar for controlled egress. Claude cannot reach the host filesystem or network directly. +Runs [Claude Code](https://claude.ai/code) inside an isolated Docker environment with a proxy sidecar for controlled egress. Claude cannot access the host filesystem or network directly. ## Architecture @@ -14,15 +14,13 @@ Runs [Claude Code](https://claude.ai/code) inside an isolated Docker environment │ ┌──────────────────────────────────────────────────┐ │ │ │ Docker: claude-secure │ │ │ │ │ │ -│ │ ┌─────────────┐ │ │ -│ │ │ claude │──┐ claude-internal │ │ -│ │ │ (UID 1000) │ │ (internal: true) │ │ -│ │ └─────────────┘ ├──────────────► ┌──────────┐ │ │ -│ │ ┌─────────────┐ │ │ proxy │ │ │ -│ │ │ webui │──┘ │ (UID 13) │ │ │ -│ │ │ (UID 1000) │ └────┬─────┘ │ │ -│ │ │ port 7681 │ proxy-external │ │ -│ │ └─────────────┘ │ │ │ +│ │ ┌─────────────┐ claude-internal │ │ +│ │ │ claude │ (internal: true) │ │ +│ │ │ (UID 1000) │──────────────► ┌──────────┐ │ │ +│ │ └─────────────┘ │ proxy │ │ │ +│ │ │ (UID 13) │ │ │ +│ │ └────┬─────┘ │ │ +│ │ proxy-external │ │ │ └──────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ @@ -31,7 +29,6 @@ Runs [Claude Code](https://claude.ai/code) inside an isolated Docker environment ``` - **`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-internal`** — `internal: true`; no default gateway, containers cannot reach the internet directly - **`proxy-external`** — Standard bridge; proxy sidecar only @@ -84,40 +81,15 @@ Port 54545 must be reachable from your browser for the OAuth callback: sbx ports --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. +Then run `./claude.sh start` and follow the prompt. Credentials are stored in +`~/.claude` on the host and reused on every subsequent run. ## Usage -### CLI mode - ```bash -# Start proxy, launch Claude Code in the current directory -# (pulls images from registry.zeidler.dev on first run) +# Start proxy, pull latest images, launch Claude Code in the current directory 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](https://github.com/tsl0922/ttyd), protected by HTTP basic auth. - -```bash -# 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 --publish 7681:7681/tcp - -# Stop web interface (keeps proxy running) -./claude.sh web-stop ``` ### Other commands @@ -126,27 +98,19 @@ sbx ports --publish 7681:7681/tcp ./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` builds images from source using the local Dockerfiles: ```bash ./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: @@ -161,18 +125,16 @@ acl allowed_sites dstdomain statsig.anthropic.com Rebuild after changes: ```bash -./claude.sh update ./claude.sh stop && ./claude.sh start ``` ## Security controls -| Control | claude / webui | proxy | +| Control | claude | 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 | +| Host filesystem | CWD mounted as `/workspace` | none | | Docker socket | not mounted | not mounted | -| Web auth | basic auth (ttyd `--credential`) | n/a | From 0fa411a1786819aeef1cb906faa26140136e23b3 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Wed, 15 Apr 2026 22:43:00 +0200 Subject: [PATCH 41/69] docs: updated inline docs --- claude/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/claude/Dockerfile b/claude/Dockerfile index d08c1cf..bfd7ff2 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -22,7 +22,7 @@ RUN KUBECTL_VERSION=$(curl -fsSL https://dl.k8s.io/release/stable.txt) \ # Restricts available models; cannot be bypassed via CLI flags or env vars. COPY settings.json /etc/claude-code/managed-settings.json -# Install Claude Code globally +# Install Claude Code stable release RUN curl -fsSL https://claude.ai/install.sh | ash -s stable From 6e5744b456748abb2d569f28bb0679bbf5fcf449 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Thu, 16 Apr 2026 09:38:46 +0200 Subject: [PATCH 42/69] fix claude install --- claude/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/claude/Dockerfile b/claude/Dockerfile index bfd7ff2..736ded1 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -23,7 +23,7 @@ RUN KUBECTL_VERSION=$(curl -fsSL https://dl.k8s.io/release/stable.txt) \ COPY settings.json /etc/claude-code/managed-settings.json # Install Claude Code stable release -RUN curl -fsSL https://claude.ai/install.sh | ash -s stable +RUN curl -fsSL https://claude.ai/install.sh | ash # Install MCP servers globally — entry points land in /usr/local/lib/node_modules/ From 698b06aafd55ad98bf6e1d823d4f52eb3beb196c Mon Sep 17 00:00:00 2001 From: docker-claude Date: Thu, 16 Apr 2026 09:48:42 +0200 Subject: [PATCH 43/69] fix: ash doesn't seem to work with the claude script --- claude/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/claude/Dockerfile b/claude/Dockerfile index 736ded1..915f271 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -23,7 +23,7 @@ RUN KUBECTL_VERSION=$(curl -fsSL https://dl.k8s.io/release/stable.txt) \ COPY settings.json /etc/claude-code/managed-settings.json # Install Claude Code stable release -RUN curl -fsSL https://claude.ai/install.sh | ash +RUN curl -fsSL https://claude.ai/install.sh | bash -s stable # Install MCP servers globally — entry points land in /usr/local/lib/node_modules/ From 51e7ab2b08f82d0766c68d39b7a68e2d3e05d367 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Thu, 16 Apr 2026 10:07:22 +0200 Subject: [PATCH 44/69] fix(proxy): close port-6443 allowlist bypass in squid ACLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kubectl_api ACL allowed CONNECT tunnels to any host on port 6443, bypassing the domain allowlist entirely. Remove it and require cluster hostnames to be added explicitly to allowed_sites instead. Also remove the localhost and .local entries — these aren't needed for Claude Code or the configured MCP servers. Co-Authored-By: Claude Sonnet 4.6 --- proxy/squid.conf | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/proxy/squid.conf b/proxy/squid.conf index 0cf94da..4deb96d 100644 --- a/proxy/squid.conf +++ b/proxy/squid.conf @@ -24,20 +24,17 @@ acl Safe_ports port 443 acl Safe_ports port 6443 # Kubernetes API server acl CONNECT method CONNECT -# Kubernetes API server — allow CONNECT tunnels to any cluster endpoint on :6443 -acl kubectl_api port 6443 - # ─── Egress allowlist ───────────────────────────────────────────────────────── # Add domains here as needed. Leading dot matches all subdomains. acl allowed_sites dstdomain api.anthropic.com acl allowed_sites dstdomain statsig.anthropic.com acl allowed_sites dstdomain platform.claude.com -acl allowed_sites dstdomain localhost -acl allowed_sites dstdomain .local # MCP servers acl allowed_sites dstdomain api.github.com acl allowed_sites dstdomain .gitlab.com acl allowed_sites dstdomain .atlassian.net +# Kubernetes API server — add your cluster's hostname here when using --kube +# acl allowed_sites dstdomain k8s.example.com # ─── Access rules ───────────────────────────────────────────────────────────── # Block requests to non-standard ports @@ -49,9 +46,6 @@ http_access deny CONNECT !SSL_ports # Allow HTTPS tunnels only to allowlisted destinations http_access allow CONNECT allowed_sites -# Allow kubectl to reach any Kubernetes API server on the standard port -http_access allow CONNECT kubectl_api - # Allow plain HTTP only to allowlisted destinations http_access allow allowed_sites From f68ed674d0c05ae8073cbac14a5d18ef9550f7ab Mon Sep 17 00:00:00 2001 From: docker-claude Date: Thu, 16 Apr 2026 10:13:34 +0200 Subject: [PATCH 45/69] feat: add non-technical user onboarding - setup.sh: interactive wizard for Docker check and auth configuration - launch.sh: folder-picker launcher (macOS native dialog, zenity/kdialog on Linux, text fallback) - launch.bat: Windows launcher using PowerShell folder browser + Git Bash - claude.sh: friendlier error messages with actionable links; prompt setup.sh if .env missing - hooks/pre-commit: add setup.sh and launch.sh to executable enforcement - README: add Quick Start section aimed at non-technical users Co-Authored-By: Claude Sonnet 4.6 --- README.md | 76 ++++++++++++++++++-------------- claude.sh | 36 ++++++++++----- hooks/pre-commit | 2 +- launch.bat | 55 +++++++++++++++++++++++ launch.sh | 62 ++++++++++++++++++++++++++ setup.sh | 111 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 297 insertions(+), 45 deletions(-) create mode 100644 launch.bat create mode 100755 launch.sh create mode 100755 setup.sh diff --git a/README.md b/README.md index f87d6d2..be25819 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,38 @@ Runs [Claude Code](https://claude.ai/code) inside an isolated Docker environment with a proxy sidecar for controlled egress. Claude cannot access the host filesystem or network directly. +## Quick Start + +**1. Install Docker Desktop** + +Download and install [Docker Desktop](https://www.docker.com/products/docker-desktop/) for your platform. It's free and includes everything needed — no extra tools required. + +**2. Download this repo** + +Clone or download and unzip this repository somewhere on your machine. + +**3. Run setup** + +- **macOS / Linux:** Open a terminal, navigate to the folder, and run: + ```bash + ./setup.sh + ``` +- **Windows:** Double-click `launch.bat` — it will run setup automatically on first launch. + +Setup will ask how you want to authenticate (API key, subscription token, or browser login) and save your settings. + +**4. Start Claude** + +- **macOS / Linux:** Double-click `launch.sh`, or run it from a terminal: + ```bash + ./launch.sh + ``` + A folder picker will appear — select the project you want Claude to work on. + +- **Windows:** Double-click `launch.bat`. + +--- + ## Architecture ``` @@ -35,31 +67,17 @@ Runs [Claude Code](https://claude.ai/code) inside an isolated Docker environment ## Prerequisites -- Docker Engine 24+ -- Docker Compose v2 plugin (`docker compose version`) - -## Setup - -```bash -# 1. Clone / copy this repo -git clone 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 -``` +- [Docker Desktop](https://www.docker.com/products/docker-desktop/) (includes Docker Engine and Compose) ## Authentication -Three options — pick one and set it in `.env`: +Three options — `./setup.sh` will guide you through picking one: ### Option 1 — API key ```bash ANTHROPIC_API_KEY=sk-ant-... ``` +Get a key at [console.anthropic.com](https://console.anthropic.com/settings/keys). ### Option 2 — OAuth token (subscription, headless-friendly) @@ -67,7 +85,7 @@ Run this **on your host** (not inside the container) to generate a 1-year token: ```bash claude setup-token ``` -Then set the printed token in `.env`: +Then paste the token into setup, or set it manually in `.env`: ```bash CLAUDE_CODE_OAUTH_TOKEN=... ``` @@ -75,26 +93,22 @@ 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: - -```bash -sbx ports --publish 54545:54545/tcp -``` - -Then run `./claude.sh start` and follow the prompt. Credentials are stored in -`~/.claude` on the host and reused on every subsequent run. +Port 54545 must be reachable from your browser for the OAuth callback. ## Usage +### Normal use + ```bash -# Start proxy, pull latest images, launch Claude Code in the current directory -cd ~/myproject -./claude.sh start +./launch.sh # folder picker → starts Claude in the selected directory ``` -### Other commands +### CLI / power users ```bash +cd ~/myproject +./claude.sh start + ./claude.sh stop # Stop and remove all containers ./claude.sh update # Pull latest images from the registry ./claude.sh logs # Tail proxy logs @@ -104,8 +118,6 @@ cd ~/myproject ### Building locally -`build.sh` builds images from source using the local Dockerfiles: - ```bash ./build.sh # build with layer cache ./build.sh --no-cache # force full rebuild diff --git a/claude.sh b/claude.sh index a78dc25..7a7f3a0 100755 --- a/claude.sh +++ b/claude.sh @@ -16,24 +16,36 @@ error() { echo -e "${RED}[-]${NC} $*" >&2; } # ─── Helpers ────────────────────────────────────────────────────────────────── check_deps() { - command -v docker &>/dev/null \ - || { error "Docker is not installed. https://docs.docker.com/get-docker/"; exit 1; } - docker compose version &>/dev/null 2>&1 \ - || { error "Docker Compose v2 plugin is required."; exit 1; } + if ! command -v docker &>/dev/null; then + error "Docker is not installed." + error " → Download Docker Desktop (free): https://www.docker.com/products/docker-desktop/" + exit 1 + fi + if ! docker info &>/dev/null 2>&1; then + error "Docker is not running." + error " → Open Docker Desktop and wait for it to finish starting, then try again." + exit 1 + fi + if ! docker compose version &>/dev/null 2>&1; then + error "Docker Compose is not available." + error " → Download Docker Desktop (includes Compose): https://www.docker.com/products/docker-desktop/" + exit 1 + fi } load_env() { local env_file="$SCRIPT_DIR/.env" - if [[ -f "$env_file" ]]; then - # shellcheck disable=SC1090 - set -a; source "$env_file"; set +a + if [[ ! -f "$env_file" ]]; then + warn "Not set up yet. Run ./setup.sh first." + exit 1 fi + # shellcheck disable=SC1090 + set -a; source "$env_file"; set +a if [[ -z "${ANTHROPIC_API_KEY:-}" && -z "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]]; then - warn "No ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN found." - warn "Claude Code will prompt you to authenticate on first run." - warn " Option 1 (API key): set ANTHROPIC_API_KEY in .env" - warn " Option 2 (token): run 'claude setup-token' and set CLAUDE_CODE_OAUTH_TOKEN in .env" - warn " Option 3 (browser): log in when prompted; port 54545 must be reachable from your browser." + warn "No credentials found — Claude will ask you to log in via browser." + warn "A login URL will appear below. Open it to authenticate." + warn "(To skip this prompt in future, run ./setup.sh to configure credentials.)" + echo "" fi } diff --git a/hooks/pre-commit b/hooks/pre-commit index 9e6e3ae..cf9ee40 100755 --- a/hooks/pre-commit +++ b/hooks/pre-commit @@ -2,7 +2,7 @@ # Ensure control scripts stay executable. set -euo pipefail -SCRIPTS=(claude.sh build.sh hooks/pre-commit) +SCRIPTS=(claude.sh build.sh setup.sh launch.sh hooks/pre-commit) for f in "${SCRIPTS[@]}"; do if [[ -f "$f" && ! -x "$f" ]]; then diff --git a/launch.bat b/launch.bat new file mode 100644 index 0000000..21d3d13 --- /dev/null +++ b/launch.bat @@ -0,0 +1,55 @@ +@echo off +:: launch.bat — Pick a project folder and start Claude Code (Windows) +setlocal enabledelayedexpansion + +set "SCRIPT_DIR=%~dp0" +set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%" + +:: ── Check for bash (Git Bash or WSL) ───────────────────────────────────────── +where bash >nul 2>&1 +if %errorlevel% neq 0 ( + echo Git Bash is required to run docker-claude on Windows. + echo. + echo Download it at: https://git-scm.com/download/win + echo Install with default options, then double-click this file again. + pause + exit /b 1 +) + +:: ── First-time setup ────────────────────────────────────────────────────────── +if not exist "%SCRIPT_DIR%\.env" ( + echo Looks like this is your first time. Running setup... + echo. + bash "%SCRIPT_DIR%/setup.sh" + if %errorlevel% neq 0 ( pause & exit /b 1 ) + echo. +) + +:: ── Folder picker via PowerShell ────────────────────────────────────────────── +set "PROJECT_FOLDER=" +for /f "usebackq tokens=*" %%i in (`powershell -NoProfile -Command ^ + "Add-Type -AssemblyName System.Windows.Forms; ^ + $d = New-Object System.Windows.Forms.FolderBrowserDialog; ^ + $d.Description = 'Select the project folder to work on'; ^ + $d.RootFolder = 'MyComputer'; ^ + $d.ShowNewFolderButton = $false; ^ + if ($d.ShowDialog() -eq 'OK') { Write-Output $d.SelectedPath } else { exit 1 }"`) do ( + set "PROJECT_FOLDER=%%i" +) + +if not defined PROJECT_FOLDER ( + echo No folder selected. Exiting. + pause + exit /b 1 +) + +:: ── Launch ──────────────────────────────────────────────────────────────────── +:: Convert Windows path to Unix path for bash +for /f "usebackq tokens=*" %%i in (`bash -c "cygpath -u '!PROJECT_FOLDER!'"`) do ( + set "UNIX_FOLDER=%%i" +) + +bash -c "cd '!UNIX_FOLDER!' && '!SCRIPT_DIR:/=\..\..\!/claude.sh' start" 2>nul || ^ +bash -c "cd '!UNIX_FOLDER!' && bash '$(cygpath -u '!SCRIPT_DIR!')/claude.sh' start" + +pause diff --git a/launch.sh b/launch.sh new file mode 100755 index 0000000..d987957 --- /dev/null +++ b/launch.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# launch.sh — Pick a project folder and start Claude Code +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# ─── First-time setup ───────────────────────────────────────────────────────── +if [[ ! -f "$SCRIPT_DIR/.env" ]]; then + echo "Looks like this is your first time. Running setup..." + echo "" + "$SCRIPT_DIR/setup.sh" || exit 1 + echo "" +fi + +# ─── Folder picker ──────────────────────────────────────────────────────────── +pick_folder() { + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS — native Finder dialog + osascript -e \ + 'tell application "Finder" to POSIX path of (choose folder with prompt "Select the project folder to work on:")' \ + 2>/dev/null | tr -d '\n' + elif command -v zenity &>/dev/null; then + # Linux — GNOME/GTK dialog + zenity --file-selection --directory \ + --title="Select your project folder" 2>/dev/null + elif command -v kdialog &>/dev/null; then + # Linux — KDE dialog + kdialog --getexistingdirectory "$HOME" \ + --title "Select your project folder" 2>/dev/null + else + echo "" + fi +} + +folder=$(pick_folder || true) + +# Fallback: text prompt (no GUI available, or user cancelled dialog) +if [[ -z "$folder" ]]; then + echo "Enter the path to your project folder" + echo "(Tip: you can drag the folder into this window, then press Enter)" + echo "" + read -rp "> " folder + # Clean up: strip surrounding quotes and trailing whitespace from drag-and-drop + folder="${folder%"${folder##*[![:space:]]}"}" + folder="${folder#\'}" ; folder="${folder%\'}" + folder="${folder#\"}" ; folder="${folder%\"}" + # Expand ~ to home directory + folder="${folder/#\~/$HOME}" +fi + +if [[ -z "$folder" ]]; then + echo "No folder selected. Exiting." + exit 1 +fi + +if [[ ! -d "$folder" ]]; then + echo "Folder not found: $folder" + exit 1 +fi + +cd "$folder" +exec "$SCRIPT_DIR/claude.sh" start diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..3f9d380 --- /dev/null +++ b/setup.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +# setup.sh — First-time setup wizard for docker-claude +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="$SCRIPT_DIR/.env" + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BOLD='\033[1m'; NC='\033[0m' +info() { echo -e "${GREEN}[+]${NC} $*"; } +warn() { echo -e "${YELLOW}[!]${NC} $*"; } +error() { echo -e "${RED}[✗]${NC} $*" >&2; } +step() { echo -e "\n${BOLD}$*${NC}"; } + +# ─── Check Docker ───────────────────────────────────────────────────────────── +check_docker() { + step "Checking Docker..." + + if ! command -v docker &>/dev/null; then + error "Docker is not installed." + echo " → Download Docker Desktop (free): https://www.docker.com/products/docker-desktop/" + echo " It includes everything you need — no extra tools required." + exit 1 + fi + + if ! docker info &>/dev/null 2>&1; then + error "Docker is installed but not running." + echo " → Open Docker Desktop and wait for the whale icon to stop animating," + echo " then run this setup again." + exit 1 + fi + + if ! docker compose version &>/dev/null 2>&1; then + error "Docker Compose is not available." + echo " → Download Docker Desktop (includes Compose): https://www.docker.com/products/docker-desktop/" + exit 1 + fi + + info "Docker is ready." +} + +# ─── Auth setup ─────────────────────────────────────────────────────────────── +setup_auth() { + step "Authentication" + echo " How would you like to sign in to Claude?" + echo "" + echo " 1) Anthropic API key (pay-per-use)" + echo " Get one at: https://console.anthropic.com/settings/keys" + echo "" + echo " 2) Claude subscription (Claude Pro or Max)" + echo " Generates a token from your existing subscription." + echo "" + echo " 3) Browser login (sign in when Claude first starts)" + echo "" + read -rp " Choice [1/2/3, default: 3]: " choice + choice="${choice:-3}" + + case "$choice" in + 1) + echo "" + read -rp " Paste your API key (sk-ant-...): " api_key + if [[ -z "$api_key" ]]; then + error "No API key entered. Run setup again when you have one." + exit 1 + fi + echo "ANTHROPIC_API_KEY=$api_key" > "$ENV_FILE" + ;; + 2) + echo "" + echo " You'll need to run 'claude setup-token' on your host to generate a token." + echo " If Claude Code is installed natively, run that command now and paste the result." + echo " Otherwise choose option 3 (browser login)." + echo "" + read -rp " Paste your OAuth token: " token + if [[ -z "$token" ]]; then + error "No token entered. Run setup again when you have one." + exit 1 + fi + echo "CLAUDE_CODE_OAUTH_TOKEN=$token" > "$ENV_FILE" + ;; + 3) + touch "$ENV_FILE" + warn "Browser login selected." + warn "When Claude starts for the first time, it will print a login URL." + warn "Open that URL in your browser to sign in." + ;; + *) + error "Invalid choice: $choice" + exit 1 + ;; + esac +} + +# ─── Main ───────────────────────────────────────────────────────────────────── +echo -e "\n${BOLD}docker-claude setup${NC}" +echo "────────────────────" + +if [[ -f "$ENV_FILE" ]]; then + warn ".env already exists (setup was already run)." + read -rp " Reconfigure authentication? [y/N]: " confirm + if [[ "${confirm,,}" != "y" ]]; then + info "Setup skipped. Run ./launch.sh to start Claude." + exit 0 + fi +fi + +check_docker +setup_auth + +echo "" +info "Setup complete!" +info "→ Run ./launch.sh to start Claude Code." From 3aff92bd410a61a785d99cd557490408d605b528 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Thu, 16 Apr 2026 10:16:23 +0200 Subject: [PATCH 46/69] chore: replace Docker Desktop references with open-source alternatives Docker Desktop requires a commercial licence for business use. Replace all references with free alternatives: - macOS: Rancher Desktop (GUI) or Colima (CLI) - Linux: Docker Engine CE (no Desktop needed at all) - Windows: Rancher Desktop or WSL2 + Docker Engine setup.sh detects the OS and shows platform-specific install instructions. claude.sh defers to setup.sh for install hints to avoid duplication. README documents all options including a WSL2 setup walkthrough. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 30 +++++++++++++++++++++++++++--- claude.sh | 9 +++------ setup.sh | 47 ++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 72 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index be25819..7eaef5d 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,17 @@ Runs [Claude Code](https://claude.ai/code) inside an isolated Docker environment ## Quick Start -**1. Install Docker Desktop** +**1. Install a Docker runtime** -Download and install [Docker Desktop](https://www.docker.com/products/docker-desktop/) for your platform. It's free and includes everything needed — no extra tools required. +Pick the free, open-source option for your platform: + +| Platform | Recommended | Alternative | +|---|---|---| +| macOS | [Rancher Desktop](https://rancherdesktop.io/) (GUI) | [Colima](https://github.com/abiosoft/colima) (CLI): `brew install colima docker docker-compose && colima start` | +| Linux | Docker Engine: `curl -fsSL https://get.docker.com \| sh` | [Rancher Desktop](https://rancherdesktop.io/) | +| Windows | [Rancher Desktop](https://rancherdesktop.io/) (GUI) | WSL2 + Docker Engine (see below) | + +> **Note:** Docker Desktop is not listed — it requires a commercial licence for business use. **2. Download this repo** @@ -67,7 +75,13 @@ Setup will ask how you want to authenticate (API key, subscription token, or bro ## Prerequisites -- [Docker Desktop](https://www.docker.com/products/docker-desktop/) (includes Docker Engine and Compose) +A Docker runtime with Compose support. Choose a free, open-source option: + +- **macOS:** [Rancher Desktop](https://rancherdesktop.io/) or [Colima](https://github.com/abiosoft/colima) +- **Linux:** [Docker Engine CE](https://docs.docker.com/engine/install/) (`curl -fsSL https://get.docker.com | sh`) +- **Windows:** [Rancher Desktop](https://rancherdesktop.io/) or WSL2 + Docker Engine + +> Docker Desktop is not recommended — it requires a commercial licence for business use. ## Authentication @@ -116,6 +130,16 @@ cd ~/myproject ./claude.sh shell # Debug bash shell in the Claude container ``` +### Windows: WSL2 + Docker Engine (alternative to Rancher Desktop) + +1. Install [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install): `wsl --install` in PowerShell +2. Open the Ubuntu terminal and run: + ```bash + curl -fsSL https://get.docker.com | sh + sudo usermod -aG docker $USER + ``` +3. Log out and back in, then run `launch.bat` as usual. + ### Building locally ```bash diff --git a/claude.sh b/claude.sh index 7a7f3a0..21bede4 100755 --- a/claude.sh +++ b/claude.sh @@ -17,18 +17,15 @@ error() { echo -e "${RED}[-]${NC} $*" >&2; } # ─── Helpers ────────────────────────────────────────────────────────────────── check_deps() { if ! command -v docker &>/dev/null; then - error "Docker is not installed." - error " → Download Docker Desktop (free): https://www.docker.com/products/docker-desktop/" + error "Docker is not installed. Run ./setup.sh for install instructions." exit 1 fi if ! docker info &>/dev/null 2>&1; then - error "Docker is not running." - error " → Open Docker Desktop and wait for it to finish starting, then try again." + error "Docker is not running. Start your Docker runtime, then try again." exit 1 fi if ! docker compose version &>/dev/null 2>&1; then - error "Docker Compose is not available." - error " → Download Docker Desktop (includes Compose): https://www.docker.com/products/docker-desktop/" + error "Docker Compose is not available. Run ./setup.sh for install instructions." exit 1 fi } diff --git a/setup.sh b/setup.sh index 3f9d380..da6c37a 100755 --- a/setup.sh +++ b/setup.sh @@ -11,27 +11,64 @@ warn() { echo -e "${YELLOW}[!]${NC} $*"; } error() { echo -e "${RED}[✗]${NC} $*" >&2; } step() { echo -e "\n${BOLD}$*${NC}"; } +# ─── Platform-specific install hints ───────────────────────────────────────── +docker_install_hint() { + case "$(uname -s)" in + Darwin) + echo " Install one of the following (both are free and open source):" + echo " • Rancher Desktop (GUI, easiest): https://rancherdesktop.io/" + echo " • Colima (CLI): brew install colima docker docker-compose && colima start" + ;; + Linux) + echo " Install Docker Engine (free, no licensing restrictions):" + echo " curl -fsSL https://get.docker.com | sh" + echo " sudo usermod -aG docker \$USER # then log out and back in" + ;; + *) + # Windows / Git Bash / WSL + echo " Install one of the following (both are free and open source):" + echo " • Rancher Desktop (GUI, easiest): https://rancherdesktop.io/" + echo " • WSL2 + Docker Engine: install Ubuntu from the Microsoft Store," + echo " then run: curl -fsSL https://get.docker.com | sh" + ;; + esac +} + +docker_not_running_hint() { + case "$(uname -s)" in + Darwin|MINGW*|MSYS*|CYGWIN*) + echo " → Open Rancher Desktop (or whichever Docker runtime you installed)" + echo " and wait for it to finish starting, then run this setup again." + ;; + Linux) + echo " → Start the Docker daemon: sudo systemctl start docker" + ;; + *) + echo " → Start your Docker runtime and try again." + ;; + esac +} + # ─── Check Docker ───────────────────────────────────────────────────────────── check_docker() { step "Checking Docker..." if ! command -v docker &>/dev/null; then error "Docker is not installed." - echo " → Download Docker Desktop (free): https://www.docker.com/products/docker-desktop/" - echo " It includes everything you need — no extra tools required." + docker_install_hint exit 1 fi if ! docker info &>/dev/null 2>&1; then error "Docker is installed but not running." - echo " → Open Docker Desktop and wait for the whale icon to stop animating," - echo " then run this setup again." + docker_not_running_hint exit 1 fi if ! docker compose version &>/dev/null 2>&1; then error "Docker Compose is not available." - echo " → Download Docker Desktop (includes Compose): https://www.docker.com/products/docker-desktop/" + echo " Docker Compose is included with Rancher Desktop and Docker Engine." + docker_install_hint exit 1 fi From cf5057073351d6df109b0fed12c6d9e7a757f42d Mon Sep 17 00:00:00 2001 From: docker-claude Date: Thu, 16 Apr 2026 10:17:49 +0200 Subject: [PATCH 47/69] docs(claude.md): sync with current project state Update architecture (remove webui reference), file structure (add setup.sh, launch.sh, launch.bat, hooks/), auth (credentials now in ~/.claude), and development workflow (use setup.sh instead of manual .env copy). Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 687a431..3d15507 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,25 +13,31 @@ Two containers managed by Docker Compose: - **`claude`** — Claude Code CLI (`node:20-alpine`), runs as the built-in `node` user (UID 1000), isolated to an internal-only Docker network - **`proxy`** — Squid forward proxy (`alpine:3.21`), `squid` user, bridges the internal network to the internet with an egress allowlist -Key Docker network property: `claude-internal` has `internal: true`, meaning Docker adds no default gateway. The `claude` and `webui` containers physically cannot reach the internet without going through the `proxy` container. +Key Docker network property: `claude-internal` has `internal: true`, meaning Docker adds no default gateway. The `claude` container physically cannot reach the internet without going through the `proxy` container. Auth supports three modes (checked at startup by `claude.sh`): - `ANTHROPIC_API_KEY` — API key - `CLAUDE_CODE_OAUTH_TOKEN` — 1-year token from `claude setup-token` (headless-friendly) -- Neither set — Claude Code prompts for browser login on first run; port 54545 is published for the OAuth callback. Credentials persist in the `claude-config` named volume. +- Neither set — Claude Code prompts for browser login on first run; port 54545 is published for the OAuth callback. Credentials persist in `~/.claude` on the host. ## File Structure ``` docker-claude/ -├── claude.sh # Control script: start/stop/run/update/logs/status/shell +├── claude.sh # Control script: start/stop/update/logs/status/shell +├── setup.sh # First-time setup wizard (Docker check + auth config) +├── launch.sh # Folder-picker launcher for macOS/Linux +├── launch.bat # Folder-picker launcher for Windows +├── build.sh # Build images locally (development) ├── docker-compose.yml # Service definitions and network topology ├── claude/ -│ └── Dockerfile # Claude Code (node:20-alpine, UID 1000) +│ └── Dockerfile # Claude Code stable release (node:20-alpine, UID 1000) ├── proxy/ │ ├── Dockerfile # Squid proxy sidecar (alpine:3.21, squid user) │ └── squid.conf # Squid ACL config — egress allowlist lives here -├── .env.example # Template for ANTHROPIC_API_KEY, WEBUI_PASSWORD, etc. +├── hooks/ +│ └── pre-commit # Enforces executable bit on shell scripts +├── .env.example # Template for credentials and options ├── .gitignore # Excludes .env and logs ├── .dockerignore # Keeps .env out of build context └── README.md # User documentation @@ -40,8 +46,7 @@ docker-claude/ ## Development Workflow ```bash -chmod +x claude.sh build.sh -cp .env.example .env # set ANTHROPIC_API_KEY +./setup.sh # first-time: configure Docker check + auth cd /path/to/project && ./claude.sh start # start proxy + launch Claude (pulls images, mounts CWD) ./claude.sh update # pull latest images from registry ./build.sh # build images locally (development) @@ -49,7 +54,7 @@ cd /path/to/project && ./claude.sh start # start proxy + launch Claude (pulls i ## Git Hooks -A pre-commit hook lives in `hooks/` and enforces the executable bit on `claude.sh` and `build.sh`. Activate it once after cloning: +A pre-commit hook lives in `hooks/` and enforces the executable bit on all shell scripts. Activate it once after cloning: ```bash git config core.hooksPath hooks From 530def213bd5679c15a7fd2a4c20d1b8367da857 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Thu, 16 Apr 2026 11:53:16 +0200 Subject: [PATCH 48/69] feat(ci): add Trivy container security scanning before push Add a scan job between check-docker and build-and-push. Builds each image locally (no push, current platform only), runs Trivy v0.35.0 against it, and fails on unfixed HIGH/CRITICAL CVEs. build-and-push only runs if both scans pass. Co-Authored-By: Claude Sonnet 4.6 --- .forgejo/workflows/docker-build.yml | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/.forgejo/workflows/docker-build.yml b/.forgejo/workflows/docker-build.yml index 3f76b5d..12af44d 100644 --- a/.forgejo/workflows/docker-build.yml +++ b/.forgejo/workflows/docker-build.yml @@ -33,7 +33,43 @@ jobs: fi done + scan: + needs: check-docker + runs-on: docker-cli + services: + docker: + image: registry.zeidler.dev/docker-hub/catthehacker/ubuntu:act-latest + options: --privileged + container: + image: registry.zeidler.dev/docker-hub/catthehacker/ubuntu:act-latest + steps: + - name: Checkout the repo + uses: actions/checkout@v4 + - name: Build proxy image for scanning + run: docker build -t scan/proxy:latest ./proxy + - name: Scan proxy image + uses: aquasecurity/trivy-action@v0.35.0 + with: + image-ref: scan/proxy:latest + format: table + exit-code: '1' + severity: HIGH,CRITICAL + ignore-unfixed: true + vuln-type: os,library + - name: Build claude image for scanning + run: docker build -t scan/claude:latest ./claude + - name: Scan claude image + uses: aquasecurity/trivy-action@v0.35.0 + with: + image-ref: scan/claude:latest + format: table + exit-code: '1' + severity: HIGH,CRITICAL + ignore-unfixed: true + vuln-type: os,library + build-and-push: + needs: scan runs-on: docker-cli services: docker: From e6b46087b3df7029e8df0e712f4c154dd43d7689 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Thu, 16 Apr 2026 11:59:24 +0200 Subject: [PATCH 49/69] feat(ci): generate SBOMs in scan job and attach attestations on push Scan job: run Trivy before the security gate to emit a CycloneDX SBOM for each image (exit-code 0), then run the HIGH/CRITICAL gate as before. SBOMs are uploaded as a pipeline artifact (90-day retention) with if: always() so they're available even when the security gate fails. Build job: add sbom: true and provenance: true to both build-push steps so BuildKit attaches SBOM and provenance attestations to the image manifest in the registry. Co-Authored-By: Claude Sonnet 4.6 --- .forgejo/workflows/docker-build.yml | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/.forgejo/workflows/docker-build.yml b/.forgejo/workflows/docker-build.yml index 12af44d..43ea694 100644 --- a/.forgejo/workflows/docker-build.yml +++ b/.forgejo/workflows/docker-build.yml @@ -45,8 +45,19 @@ jobs: steps: - name: Checkout the repo uses: actions/checkout@v4 + - name: Build proxy image for scanning run: docker build -t scan/proxy:latest ./proxy + + - name: Generate proxy SBOM + uses: aquasecurity/trivy-action@v0.35.0 + with: + image-ref: scan/proxy:latest + format: cyclonedx + output: sbom-proxy.cdx.json + exit-code: '0' + vuln-type: os,library + - name: Scan proxy image uses: aquasecurity/trivy-action@v0.35.0 with: @@ -56,8 +67,19 @@ jobs: severity: HIGH,CRITICAL ignore-unfixed: true vuln-type: os,library + - name: Build claude image for scanning run: docker build -t scan/claude:latest ./claude + + - name: Generate claude SBOM + uses: aquasecurity/trivy-action@v0.35.0 + with: + image-ref: scan/claude:latest + format: cyclonedx + output: sbom-claude.cdx.json + exit-code: '0' + vuln-type: os,library + - name: Scan claude image uses: aquasecurity/trivy-action@v0.35.0 with: @@ -68,6 +90,16 @@ jobs: ignore-unfixed: true vuln-type: os,library + - name: Upload SBOMs + if: always() + uses: actions/upload-artifact@v4 + with: + name: sboms-${{ env.GITHUB_RUN_NUMBER }} + path: | + sbom-proxy.cdx.json + sbom-claude.cdx.json + retention-days: 90 + build-and-push: needs: scan runs-on: docker-cli @@ -96,6 +128,8 @@ jobs: with: context: proxy push: true + sbom: true + provenance: true platforms: linux/amd64, linux/arm64 tags: | ${{ vars.REGISTRY_URL }}/docker-public/${{ env.GITHUB_REPOSITORY }}-proxy:0.1.${{ env.GITHUB_RUN_NUMBER }} @@ -105,6 +139,8 @@ jobs: with: context: claude push: true + sbom: true + provenance: true platforms: linux/amd64, linux/arm64 tags: | ${{ vars.REGISTRY_URL }}/docker-public/${{ env.GITHUB_REPOSITORY }}-claude:0.1.${{ env.GITHUB_RUN_NUMBER }} From 4a0f2e90fc07d4a70e188d904faa3b146270db6a Mon Sep 17 00:00:00 2001 From: docker-claude Date: Thu, 16 Apr 2026 12:03:17 +0200 Subject: [PATCH 50/69] fix(ci): replace trivy-action with docker run to fix missing binary trivy-action@v0.35.0 expects trivy pre-installed in the runner environment. Switch to docker run aquasec/trivy:0.69.3 which uses the Docker daemon already available in the pipeline. Pin version via TRIVY_IMAGE env var. Co-Authored-By: Claude Sonnet 4.6 --- .forgejo/workflows/docker-build.yml | 64 +++++++++++++++-------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/.forgejo/workflows/docker-build.yml b/.forgejo/workflows/docker-build.yml index 43ea694..a213ea4 100644 --- a/.forgejo/workflows/docker-build.yml +++ b/.forgejo/workflows/docker-build.yml @@ -10,6 +10,8 @@ env: # whichever you use to reach it from your desktop/laptop FORGEJO_HOST: code.zeidler.dev HELM_EXPERIMENTAL_OCI: 1 + TRIVY_IMAGE: aquasec/trivy:0.69.3 + jobs: check-docker: runs-on: docker-cli @@ -50,45 +52,47 @@ jobs: run: docker build -t scan/proxy:latest ./proxy - name: Generate proxy SBOM - uses: aquasecurity/trivy-action@v0.35.0 - with: - image-ref: scan/proxy:latest - format: cyclonedx - output: sbom-proxy.cdx.json - exit-code: '0' - vuln-type: os,library + run: | + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$PWD":/output \ + ${{ env.TRIVY_IMAGE }} \ + image --exit-code 0 --vuln-type os,library \ + --format cyclonedx --output /output/sbom-proxy.cdx.json \ + scan/proxy:latest - name: Scan proxy image - uses: aquasecurity/trivy-action@v0.35.0 - with: - image-ref: scan/proxy:latest - format: table - exit-code: '1' - severity: HIGH,CRITICAL - ignore-unfixed: true - vuln-type: os,library + run: | + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + ${{ env.TRIVY_IMAGE }} \ + image --exit-code 1 --severity HIGH,CRITICAL \ + --ignore-unfixed --vuln-type os,library \ + --format table \ + scan/proxy:latest - name: Build claude image for scanning run: docker build -t scan/claude:latest ./claude - name: Generate claude SBOM - uses: aquasecurity/trivy-action@v0.35.0 - with: - image-ref: scan/claude:latest - format: cyclonedx - output: sbom-claude.cdx.json - exit-code: '0' - vuln-type: os,library + run: | + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$PWD":/output \ + ${{ env.TRIVY_IMAGE }} \ + image --exit-code 0 --vuln-type os,library \ + --format cyclonedx --output /output/sbom-claude.cdx.json \ + scan/claude:latest - name: Scan claude image - uses: aquasecurity/trivy-action@v0.35.0 - with: - image-ref: scan/claude:latest - format: table - exit-code: '1' - severity: HIGH,CRITICAL - ignore-unfixed: true - vuln-type: os,library + run: | + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + ${{ env.TRIVY_IMAGE }} \ + image --exit-code 1 --severity HIGH,CRITICAL \ + --ignore-unfixed --vuln-type os,library \ + --format table \ + scan/claude:latest - name: Upload SBOMs if: always() From 94dadbbe8ed437403194cf599bd26a9d92aa45dc Mon Sep 17 00:00:00 2001 From: docker-claude Date: Thu, 16 Apr 2026 12:04:24 +0200 Subject: [PATCH 51/69] chore(ci): pull trivy from registry mirror Co-Authored-By: Claude Sonnet 4.6 --- .forgejo/workflows/docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/docker-build.yml b/.forgejo/workflows/docker-build.yml index a213ea4..97b97cd 100644 --- a/.forgejo/workflows/docker-build.yml +++ b/.forgejo/workflows/docker-build.yml @@ -10,7 +10,7 @@ env: # whichever you use to reach it from your desktop/laptop FORGEJO_HOST: code.zeidler.dev HELM_EXPERIMENTAL_OCI: 1 - TRIVY_IMAGE: aquasec/trivy:0.69.3 + TRIVY_IMAGE: registry.zeidler.dev/docker-hub/aquasec/trivy:0.69.3 jobs: check-docker: From edeae9dc4b474cdadaaee24e8fd7e921fb06802a Mon Sep 17 00:00:00 2001 From: docker-claude Date: Thu, 16 Apr 2026 22:46:52 +0200 Subject: [PATCH 52/69] security: use dhi image for nodejs --- claude/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/claude/Dockerfile b/claude/Dockerfile index 915f271..0a1be39 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine +FROM registry.zeidler.dev/docker-dhi/node:25-alpine3.23-dev # Install runtime dependencies RUN apk add --no-cache \ From 89a3d3dae5718dea3fc15544283e8dbf19a43ba6 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Thu, 16 Apr 2026 22:52:33 +0200 Subject: [PATCH 53/69] security: use dhi image for the proxy as well --- proxy/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/proxy/Dockerfile b/proxy/Dockerfile index 7382d3c..5d494e6 100644 --- a/proxy/Dockerfile +++ b/proxy/Dockerfile @@ -1,11 +1,11 @@ -FROM alpine:3.21 +FROM registry.zeidler.dev/docker-dhi/alpine-base:3.23-alpine3.23-dev # squid: proxy. netcat-openbsd: health check RUN apk add --no-cache squid netcat-openbsd # squid user is created by the package (apk add squid) RUN mkdir -p /var/cache/squid /var/log/squid \ - && chown -R squid:squid /var/cache/squid /var/log/squid /etc/squid + && chown -R squid:squid /var/cache/squid /var/log/squid /etc/squid COPY --chown=squid:squid squid.conf /etc/squid/squid.conf @@ -14,6 +14,6 @@ USER squid EXPOSE 3128 HEALTHCHECK --interval=10s --timeout=5s --retries=3 \ - CMD nc -z 127.0.0.1 3128 || exit 1 + CMD nc -z 127.0.0.1 3128 || exit 1 CMD ["squid", "-N", "-f", "/etc/squid/squid.conf"] From 484e0fecb83d3476b45446b190c64850e1fa45b6 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Mon, 20 Apr 2026 14:28:45 +0200 Subject: [PATCH 54/69] revert(docker): switch back to node:20-alpine base image dhi.io is unreachable on the company network, blocking apk during build. Trivy scanning in CI provides vulnerability coverage in the meantime. Co-Authored-By: Claude Sonnet 4.6 --- claude/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/claude/Dockerfile b/claude/Dockerfile index 0a1be39..915f271 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -1,4 +1,4 @@ -FROM registry.zeidler.dev/docker-dhi/node:25-alpine3.23-dev +FROM node:20-alpine # Install runtime dependencies RUN apk add --no-cache \ From 19c59a2fb391b4f0a1bd8b2fca44cf714749304b Mon Sep 17 00:00:00 2001 From: docker-claude Date: Mon, 20 Apr 2026 15:14:58 +0200 Subject: [PATCH 55/69] fix(docker): upgrade npm to remediate 11 HIGH CVEs in bundled dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All findings are in npm's own bundled packages (cross-spawn, glob, minimatch, tar). Upgrading npm to latest pulls in the patched versions: - cross-spawn ≥7.0.5 (CVE-2024-21538) - glob ≥10.5.0 (CVE-2025-64756) - minimatch ≥9.0.6 (CVE-2026-26996, CVE-2026-27903, CVE-2026-27904) - tar ≥7.5.11 (CVE-2026-23745, CVE-2026-23950, CVE-2026-24842, CVE-2026-26960, CVE-2026-29786, CVE-2026-31802) Co-Authored-By: Claude Sonnet 4.6 --- claude/Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/claude/Dockerfile b/claude/Dockerfile index 915f271..cd9cce4 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -1,5 +1,9 @@ FROM node:20-alpine +# Upgrade npm to pull in patched bundled deps (cross-spawn, glob, minimatch, tar) +# CVEs: CVE-2024-21538, CVE-2025-64756, CVE-2026-26996/27903/27904, CVE-2026-23745/23950/24842/26960/29786/31802 +RUN npm install -g npm@latest + # Install runtime dependencies RUN apk add --no-cache \ git \ From e056e5c006e8c712415895b059412582802bcce2 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Mon, 20 Apr 2026 15:15:51 +0200 Subject: [PATCH 56/69] chore(docker): pin npm to 11.12.1 Co-Authored-By: Claude Sonnet 4.6 --- claude/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/claude/Dockerfile b/claude/Dockerfile index cd9cce4..11aef62 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -2,7 +2,7 @@ FROM node:20-alpine # Upgrade npm to pull in patched bundled deps (cross-spawn, glob, minimatch, tar) # CVEs: CVE-2024-21538, CVE-2025-64756, CVE-2026-26996/27903/27904, CVE-2026-23745/23950/24842/26960/29786/31802 -RUN npm install -g npm@latest +RUN npm install -g npm@11.12.1 # Install runtime dependencies RUN apk add --no-cache \ From ec329ca616d660d005d55e3b45bdaf4599eba8c6 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Mon, 20 Apr 2026 15:16:52 +0200 Subject: [PATCH 57/69] chore(docker): upgrade base image to node:24-alpine (LTS) Node 24 (Krypton) is the current LTS release. Co-Authored-By: Claude Sonnet 4.6 --- claude/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/claude/Dockerfile b/claude/Dockerfile index 11aef62..f31efec 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine +FROM node:24-alpine # Upgrade npm to pull in patched bundled deps (cross-spawn, glob, minimatch, tar) # CVEs: CVE-2024-21538, CVE-2025-64756, CVE-2026-26996/27903/27904, CVE-2026-23745/23950/24842/26960/29786/31802 From a566b463a98cee02eb8b857461b3e288cbe5a884 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Mon, 20 Apr 2026 15:17:15 +0200 Subject: [PATCH 58/69] docs: update node:20-alpine references to node:24-alpine Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 6 +++--- README.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3d15507..3e394b2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ This file provides context and guidance for working with this project. Two containers managed by Docker Compose: -- **`claude`** — Claude Code CLI (`node:20-alpine`), runs as the built-in `node` user (UID 1000), isolated to an internal-only Docker network +- **`claude`** — Claude Code CLI (`node:24-alpine`), runs as the built-in `node` user (UID 1000), isolated to an internal-only Docker network - **`proxy`** — Squid forward proxy (`alpine:3.21`), `squid` user, bridges the internal network to the internet with an egress allowlist Key Docker network property: `claude-internal` has `internal: true`, meaning Docker adds no default gateway. The `claude` container physically cannot reach the internet without going through the `proxy` container. @@ -31,7 +31,7 @@ docker-claude/ ├── build.sh # Build images locally (development) ├── docker-compose.yml # Service definitions and network topology ├── claude/ -│ └── Dockerfile # Claude Code stable release (node:20-alpine, UID 1000) +│ └── Dockerfile # Claude Code stable release (node:24-alpine, UID 1000) ├── proxy/ │ ├── Dockerfile # Squid proxy sidecar (alpine:3.21, squid user) │ └── squid.conf # Squid ACL config — egress allowlist lives here @@ -63,7 +63,7 @@ git config core.hooksPath hooks ## Coding Standards - Shell scripts use `set -euo pipefail` -- Dockerfiles use Alpine (`node:20-alpine`, `alpine:3.21`) for minimal attack surface +- Dockerfiles use Alpine (`node:24-alpine`, `alpine:3.21`) for minimal attack surface - Alpine packages use `apk add --no-cache`; no apt cache cleanup layer needed - No capabilities granted; `no-new-privileges` on all containers - `.env` is never committed (enforced by `.gitignore` and `.dockerignore`) diff --git a/README.md b/README.md index 7eaef5d..af10e9d 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ Setup will ask how you want to authenticate (API key, subscription token, or bro └──────────────────────────────────────────────────────────┘ ``` -- **`claude`** — Claude Code CLI (`node:20-alpine`), runs as the built-in `node` user (UID 1000), on `claude-internal` only +- **`claude`** — Claude Code CLI (`node:24-alpine`), runs as the built-in `node` user (UID 1000), on `claude-internal` only - **`proxy`** — Squid forward proxy (`alpine:3.21`), bridges `claude-internal` ↔ internet with egress allowlist - **`claude-internal`** — `internal: true`; no default gateway, containers cannot reach the internet directly - **`proxy-external`** — Standard bridge; proxy sidecar only From 53325c4fcd5c2b62bc1250d75d86c3640323a6ee Mon Sep 17 00:00:00 2001 From: docker-claude Date: Mon, 20 Apr 2026 15:17:58 +0200 Subject: [PATCH 59/69] stuff --- claude.sh | 0 hooks/pre-commit | 0 launch.sh | 0 setup.sh | 0 4 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 claude.sh mode change 100755 => 100644 hooks/pre-commit mode change 100755 => 100644 launch.sh mode change 100755 => 100644 setup.sh diff --git a/claude.sh b/claude.sh old mode 100755 new mode 100644 diff --git a/hooks/pre-commit b/hooks/pre-commit old mode 100755 new mode 100644 diff --git a/launch.sh b/launch.sh old mode 100755 new mode 100644 diff --git a/setup.sh b/setup.sh old mode 100755 new mode 100644 From eb5f240d3e5456f971213077578f6b93ee377036 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Mon, 20 Apr 2026 15:28:20 +0200 Subject: [PATCH 60/69] fix(docker): patch transitive CVEs in MCP server dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MCP servers bundle their own copies of vulnerable packages. After global install, patch nested node_modules in each server directly: - @modelcontextprotocol/sdk 1.0.1 → 1.25.2 (CVE-2025-66414, CVE-2026-0621) - picomatch 4.0.3 → 4.0.4 (CVE-2026-33671) Co-Authored-By: Claude Sonnet 4.6 --- claude/Dockerfile | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/claude/Dockerfile b/claude/Dockerfile index f31efec..07a3560 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -37,6 +37,22 @@ RUN npm install -g \ @aashari/mcp-server-atlassian-jira \ @aashari/mcp-server-atlassian-confluence +# Patch transitive CVEs bundled inside MCP server node_modules: +# CVE-2025-66414, CVE-2026-0621 — @modelcontextprotocol/sdk <1.25.2 +# CVE-2026-33671 — picomatch <4.0.4 +RUN for pkg_dir in \ + /usr/local/lib/node_modules/@modelcontextprotocol/server-github \ + /usr/local/lib/node_modules/@yoda.digital/gitlab-mcp-server \ + /usr/local/lib/node_modules/@aashari/mcp-server-atlassian-jira \ + /usr/local/lib/node_modules/@aashari/mcp-server-atlassian-confluence; do \ + [ -d "$pkg_dir" ] && \ + cd "$pkg_dir" && \ + npm install --no-audit --no-fund \ + @modelcontextprotocol/sdk@1.25.2 \ + picomatch@4.0.4 \ + || true; \ + done + # Workspace and Claude config dir — owned by the built-in node user (uid 1000). # Pre-creating ~/.claude ensures the named volume is initialised with the # correct ownership when first mounted (Docker copies image content into From a9ff78b49449a563de402eaaf0e9d423c009f389 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Mon, 20 Apr 2026 15:32:05 +0200 Subject: [PATCH 61/69] feat: remove MCP servers Remove all four MCP server packages from the Dockerfile along with their associated env vars (docker-compose.yml, .env.example) and egress allowlist entries (squid.conf). Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 14 -------------- claude/Dockerfile | 22 ---------------------- docker-compose.yml | 7 ------- proxy/squid.conf | 4 ---- 4 files changed, 47 deletions(-) diff --git a/.env.example b/.env.example index ad79c7b..01476c3 100644 --- a/.env.example +++ b/.env.example @@ -18,17 +18,3 @@ # Option 3: No key set — Claude Code will prompt for browser login on first run. # Port 54545 must be reachable from your browser for the OAuth callback. # Run: sbx ports --publish 54545:54545/tcp - -# ─── MCP servers (all optional) ─────────────────────────────────────────────── - -# GitHub — PAT with repo scope -# GITHUB_TOKEN=ghp_... - -# GitLab — PAT with api scope; GITLAB_URL defaults to https://gitlab.com -# GITLAB_TOKEN=glpat_... -# GITLAB_URL=https://gitlab.com - -# Jira + Confluence — shared Atlassian credentials -# ATLASSIAN_SITE_NAME=your-company # subdomain of .atlassian.net -# ATLASSIAN_USER_EMAIL=you@example.com -# ATLASSIAN_API_TOKEN=... # https://id.atlassian.com/manage-profile/security/api-tokens diff --git a/claude/Dockerfile b/claude/Dockerfile index 07a3560..a780724 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -30,28 +30,6 @@ COPY settings.json /etc/claude-code/managed-settings.json RUN curl -fsSL https://claude.ai/install.sh | bash -s stable -# Install MCP servers globally — entry points land in /usr/local/lib/node_modules/ -RUN npm install -g \ - @modelcontextprotocol/server-github \ - @yoda.digital/gitlab-mcp-server \ - @aashari/mcp-server-atlassian-jira \ - @aashari/mcp-server-atlassian-confluence - -# Patch transitive CVEs bundled inside MCP server node_modules: -# CVE-2025-66414, CVE-2026-0621 — @modelcontextprotocol/sdk <1.25.2 -# CVE-2026-33671 — picomatch <4.0.4 -RUN for pkg_dir in \ - /usr/local/lib/node_modules/@modelcontextprotocol/server-github \ - /usr/local/lib/node_modules/@yoda.digital/gitlab-mcp-server \ - /usr/local/lib/node_modules/@aashari/mcp-server-atlassian-jira \ - /usr/local/lib/node_modules/@aashari/mcp-server-atlassian-confluence; do \ - [ -d "$pkg_dir" ] && \ - cd "$pkg_dir" && \ - npm install --no-audit --no-fund \ - @modelcontextprotocol/sdk@1.25.2 \ - picomatch@4.0.4 \ - || true; \ - done # Workspace and Claude config dir — owned by the built-in node user (uid 1000). # Pre-creating ~/.claude ensures the named volume is initialised with the diff --git a/docker-compose.yml b/docker-compose.yml index 4148250..ebb9fcf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,13 +35,6 @@ services: - HTTPS_PROXY=http://proxy:3128 - ALL_PROXY=http://proxy:3128 - NO_PROXY=localhost,127.0.0.1 - # MCP server credentials — all optional; servers are skipped if unset - - GITHUB_TOKEN=${GITHUB_TOKEN:-} - - GITLAB_TOKEN=${GITLAB_TOKEN:-} - - GITLAB_URL=${GITLAB_URL:-https://gitlab.com} - - ATLASSIAN_SITE_NAME=${ATLASSIAN_SITE_NAME:-} - - ATLASSIAN_USER_EMAIL=${ATLASSIAN_USER_EMAIL:-} - - ATLASSIAN_API_TOKEN=${ATLASSIAN_API_TOKEN:-} ports: # OAuth callback — required for browser-based login (claude login) - "0.0.0.0:54545:54545" diff --git a/proxy/squid.conf b/proxy/squid.conf index 4deb96d..fc0a07e 100644 --- a/proxy/squid.conf +++ b/proxy/squid.conf @@ -29,10 +29,6 @@ acl CONNECT method CONNECT acl allowed_sites dstdomain api.anthropic.com acl allowed_sites dstdomain statsig.anthropic.com acl allowed_sites dstdomain platform.claude.com -# MCP servers -acl allowed_sites dstdomain api.github.com -acl allowed_sites dstdomain .gitlab.com -acl allowed_sites dstdomain .atlassian.net # Kubernetes API server — add your cluster's hostname here when using --kube # acl allowed_sites dstdomain k8s.example.com From 526ff6dc2e3cf4677ac6cb124990b10ac6d7866d Mon Sep 17 00:00:00 2001 From: docker-claude Date: Mon, 20 Apr 2026 15:32:29 +0200 Subject: [PATCH 62/69] Revert "feat: remove MCP servers" This reverts commit a9ff78b49449a563de402eaaf0e9d423c009f389. --- .env.example | 14 ++++++++++++++ claude/Dockerfile | 22 ++++++++++++++++++++++ docker-compose.yml | 7 +++++++ proxy/squid.conf | 4 ++++ 4 files changed, 47 insertions(+) diff --git a/.env.example b/.env.example index 01476c3..ad79c7b 100644 --- a/.env.example +++ b/.env.example @@ -18,3 +18,17 @@ # Option 3: No key set — Claude Code will prompt for browser login on first run. # Port 54545 must be reachable from your browser for the OAuth callback. # Run: sbx ports --publish 54545:54545/tcp + +# ─── MCP servers (all optional) ─────────────────────────────────────────────── + +# GitHub — PAT with repo scope +# GITHUB_TOKEN=ghp_... + +# GitLab — PAT with api scope; GITLAB_URL defaults to https://gitlab.com +# GITLAB_TOKEN=glpat_... +# GITLAB_URL=https://gitlab.com + +# Jira + Confluence — shared Atlassian credentials +# ATLASSIAN_SITE_NAME=your-company # subdomain of .atlassian.net +# ATLASSIAN_USER_EMAIL=you@example.com +# ATLASSIAN_API_TOKEN=... # https://id.atlassian.com/manage-profile/security/api-tokens diff --git a/claude/Dockerfile b/claude/Dockerfile index a780724..07a3560 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -30,6 +30,28 @@ COPY settings.json /etc/claude-code/managed-settings.json RUN curl -fsSL https://claude.ai/install.sh | bash -s stable +# Install MCP servers globally — entry points land in /usr/local/lib/node_modules/ +RUN npm install -g \ + @modelcontextprotocol/server-github \ + @yoda.digital/gitlab-mcp-server \ + @aashari/mcp-server-atlassian-jira \ + @aashari/mcp-server-atlassian-confluence + +# Patch transitive CVEs bundled inside MCP server node_modules: +# CVE-2025-66414, CVE-2026-0621 — @modelcontextprotocol/sdk <1.25.2 +# CVE-2026-33671 — picomatch <4.0.4 +RUN for pkg_dir in \ + /usr/local/lib/node_modules/@modelcontextprotocol/server-github \ + /usr/local/lib/node_modules/@yoda.digital/gitlab-mcp-server \ + /usr/local/lib/node_modules/@aashari/mcp-server-atlassian-jira \ + /usr/local/lib/node_modules/@aashari/mcp-server-atlassian-confluence; do \ + [ -d "$pkg_dir" ] && \ + cd "$pkg_dir" && \ + npm install --no-audit --no-fund \ + @modelcontextprotocol/sdk@1.25.2 \ + picomatch@4.0.4 \ + || true; \ + done # Workspace and Claude config dir — owned by the built-in node user (uid 1000). # Pre-creating ~/.claude ensures the named volume is initialised with the diff --git a/docker-compose.yml b/docker-compose.yml index ebb9fcf..4148250 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,6 +35,13 @@ services: - HTTPS_PROXY=http://proxy:3128 - ALL_PROXY=http://proxy:3128 - NO_PROXY=localhost,127.0.0.1 + # MCP server credentials — all optional; servers are skipped if unset + - GITHUB_TOKEN=${GITHUB_TOKEN:-} + - GITLAB_TOKEN=${GITLAB_TOKEN:-} + - GITLAB_URL=${GITLAB_URL:-https://gitlab.com} + - ATLASSIAN_SITE_NAME=${ATLASSIAN_SITE_NAME:-} + - ATLASSIAN_USER_EMAIL=${ATLASSIAN_USER_EMAIL:-} + - ATLASSIAN_API_TOKEN=${ATLASSIAN_API_TOKEN:-} ports: # OAuth callback — required for browser-based login (claude login) - "0.0.0.0:54545:54545" diff --git a/proxy/squid.conf b/proxy/squid.conf index fc0a07e..4deb96d 100644 --- a/proxy/squid.conf +++ b/proxy/squid.conf @@ -29,6 +29,10 @@ acl CONNECT method CONNECT acl allowed_sites dstdomain api.anthropic.com acl allowed_sites dstdomain statsig.anthropic.com acl allowed_sites dstdomain platform.claude.com +# MCP servers +acl allowed_sites dstdomain api.github.com +acl allowed_sites dstdomain .gitlab.com +acl allowed_sites dstdomain .atlassian.net # Kubernetes API server — add your cluster's hostname here when using --kube # acl allowed_sites dstdomain k8s.example.com From 9b931bcfd7cf2f107db97537db635962464002e6 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Mon, 20 Apr 2026 15:54:15 +0200 Subject: [PATCH 63/69] temporarily remove mcp servers --- claude/Dockerfile | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/claude/Dockerfile b/claude/Dockerfile index 07a3560..0de1d47 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -31,28 +31,28 @@ RUN curl -fsSL https://claude.ai/install.sh | bash -s stable # Install MCP servers globally — entry points land in /usr/local/lib/node_modules/ -RUN npm install -g \ - @modelcontextprotocol/server-github \ - @yoda.digital/gitlab-mcp-server \ - @aashari/mcp-server-atlassian-jira \ - @aashari/mcp-server-atlassian-confluence - -# Patch transitive CVEs bundled inside MCP server node_modules: -# CVE-2025-66414, CVE-2026-0621 — @modelcontextprotocol/sdk <1.25.2 -# CVE-2026-33671 — picomatch <4.0.4 -RUN for pkg_dir in \ - /usr/local/lib/node_modules/@modelcontextprotocol/server-github \ - /usr/local/lib/node_modules/@yoda.digital/gitlab-mcp-server \ - /usr/local/lib/node_modules/@aashari/mcp-server-atlassian-jira \ - /usr/local/lib/node_modules/@aashari/mcp-server-atlassian-confluence; do \ - [ -d "$pkg_dir" ] && \ - cd "$pkg_dir" && \ - npm install --no-audit --no-fund \ - @modelcontextprotocol/sdk@1.25.2 \ - picomatch@4.0.4 \ - || true; \ - done - +# RUN npm install -g \ +# @modelcontextprotocol/server-github \ +# @yoda.digital/gitlab-mcp-server \ +# @aashari/mcp-server-atlassian-jira \ +# @aashari/mcp-server-atlassian-confluence +# +# # Patch transitive CVEs bundled inside MCP server node_modules: +# # CVE-2025-66414, CVE-2026-0621 — @modelcontextprotocol/sdk <1.25.2 +# # CVE-2026-33671 — picomatch <4.0.4 +# RUN for pkg_dir in \ +# /usr/local/lib/node_modules/@modelcontextprotocol/server-github \ +# /usr/local/lib/node_modules/@yoda.digital/gitlab-mcp-server \ +# /usr/local/lib/node_modules/@aashari/mcp-server-atlassian-jira \ +# /usr/local/lib/node_modules/@aashari/mcp-server-atlassian-confluence; do \ +# [ -d "$pkg_dir" ] && \ +# cd "$pkg_dir" && \ +# npm install --no-audit --no-fund \ +# @modelcontextprotocol/sdk@1.25.2 \ +# picomatch@4.0.4 \ +# || true; \ +# done +# # Workspace and Claude config dir — owned by the built-in node user (uid 1000). # Pre-creating ~/.claude ensures the named volume is initialised with the # correct ownership when first mounted (Docker copies image content into From a79aad9fc8dd5b0d65d07590af681d05a234e9b7 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Mon, 20 Apr 2026 16:00:37 +0200 Subject: [PATCH 64/69] fix(security): remove MCP credentials from managed-settings.json; bump Trivy to 0.70.0 settings.json is COPY-ed into the image at build time. Putting MCP server config with credential env references there risks baking tokens into the image if placeholders are ever replaced with real values. Move MCP server config to ~/.claude/settings.json (runtime volume mount) instead. Managed settings now contains policy only: models, permissions, telemetry. Co-Authored-By: Claude Sonnet 4.6 --- .forgejo/workflows/docker-build.yml | 2 +- claude/settings.json | 31 ----------------------------- 2 files changed, 1 insertion(+), 32 deletions(-) diff --git a/.forgejo/workflows/docker-build.yml b/.forgejo/workflows/docker-build.yml index 97b97cd..615da18 100644 --- a/.forgejo/workflows/docker-build.yml +++ b/.forgejo/workflows/docker-build.yml @@ -10,7 +10,7 @@ env: # whichever you use to reach it from your desktop/laptop FORGEJO_HOST: code.zeidler.dev HELM_EXPERIMENTAL_OCI: 1 - TRIVY_IMAGE: registry.zeidler.dev/docker-hub/aquasec/trivy:0.69.3 + TRIVY_IMAGE: registry.zeidler.dev/docker-hub/aquasec/trivy:0.70.0 jobs: check-docker: diff --git a/claude/settings.json b/claude/settings.json index 4e2033e..175bdd4 100644 --- a/claude/settings.json +++ b/claude/settings.json @@ -6,36 +6,5 @@ "env": { "CLAUDE_CODE_ENABLE_TELEMETRY": "0" } - }, - "mcpServers": { - "github": { - "command": "mcp-server-github", - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}" - } - }, - "gitlab": { - "command": "gitlab-mcp-server", - "env": { - "GITLAB_PERSONAL_ACCESS_TOKEN": "${GITLAB_TOKEN}", - "GITLAB_URL": "${GITLAB_URL}" - } - }, - "jira": { - "command": "mcp-atlassian-jira", - "env": { - "ATLASSIAN_SITE_NAME": "${ATLASSIAN_SITE_NAME}", - "ATLASSIAN_USER_EMAIL": "${ATLASSIAN_USER_EMAIL}", - "ATLASSIAN_API_TOKEN": "${ATLASSIAN_API_TOKEN}" - } - }, - "confluence": { - "command": "mcp-atlassian-confluence", - "env": { - "ATLASSIAN_SITE_NAME": "${ATLASSIAN_SITE_NAME}", - "ATLASSIAN_USER_EMAIL": "${ATLASSIAN_USER_EMAIL}", - "ATLASSIAN_API_TOKEN": "${ATLASSIAN_API_TOKEN}" - } - } } } From 12d75b0dc2ca275dc202652a54fdf007e08cb641 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Mon, 20 Apr 2026 16:05:05 +0200 Subject: [PATCH 65/69] =?UTF-8?q?fix(docker):=20patch=20picomatch=204.0.3?= =?UTF-8?q?=20=E2=86=92=204.0.4=20(CVE-2026-33671)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit npm@11.12.1 still bundles picomatch@4.0.3. Add a find-loop after the npm upgrade to patch every occurrence in node_modules in place. Also restore and clean up the MCP server install and CVE patch blocks that were accidentally commented out. Co-Authored-By: Claude Sonnet 4.6 --- claude/Dockerfile | 55 +++++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/claude/Dockerfile b/claude/Dockerfile index 0de1d47..fdcccec 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -4,6 +4,16 @@ FROM node:24-alpine # CVEs: CVE-2024-21538, CVE-2025-64756, CVE-2026-26996/27903/27904, CVE-2026-23745/23950/24842/26960/29786/31802 RUN npm install -g npm@11.12.1 +# Fix CVE-2026-33671: upgrade picomatch 4.0.3 → 4.0.4 in every location it appears +RUN find /usr/local/lib/node_modules -name "picomatch" -type d | while read dir; do \ + ver=$(node -p "require('$dir/package.json').version" 2>/dev/null); \ + [ "$ver" = "4.0.3" ] || continue; \ + echo "Patching picomatch in $dir"; \ + prefix=$(dirname "$(dirname "$dir")"); \ + npm install --prefix "$prefix" picomatch@4.0.4 \ + --no-save --no-audit --no-fund 2>/dev/null || true; \ + done + # Install runtime dependencies RUN apk add --no-cache \ git \ @@ -29,30 +39,29 @@ COPY settings.json /etc/claude-code/managed-settings.json # Install Claude Code stable release RUN curl -fsSL https://claude.ai/install.sh | bash -s stable - # Install MCP servers globally — entry points land in /usr/local/lib/node_modules/ -# RUN npm install -g \ -# @modelcontextprotocol/server-github \ -# @yoda.digital/gitlab-mcp-server \ -# @aashari/mcp-server-atlassian-jira \ -# @aashari/mcp-server-atlassian-confluence -# -# # Patch transitive CVEs bundled inside MCP server node_modules: -# # CVE-2025-66414, CVE-2026-0621 — @modelcontextprotocol/sdk <1.25.2 -# # CVE-2026-33671 — picomatch <4.0.4 -# RUN for pkg_dir in \ -# /usr/local/lib/node_modules/@modelcontextprotocol/server-github \ -# /usr/local/lib/node_modules/@yoda.digital/gitlab-mcp-server \ -# /usr/local/lib/node_modules/@aashari/mcp-server-atlassian-jira \ -# /usr/local/lib/node_modules/@aashari/mcp-server-atlassian-confluence; do \ -# [ -d "$pkg_dir" ] && \ -# cd "$pkg_dir" && \ -# npm install --no-audit --no-fund \ -# @modelcontextprotocol/sdk@1.25.2 \ -# picomatch@4.0.4 \ -# || true; \ -# done -# +RUN npm install -g \ + @modelcontextprotocol/server-github \ + @yoda.digital/gitlab-mcp-server \ + @aashari/mcp-server-atlassian-jira \ + @aashari/mcp-server-atlassian-confluence + +# Patch transitive CVEs bundled inside MCP server node_modules: +# CVE-2025-66414, CVE-2026-0621 — @modelcontextprotocol/sdk <1.25.2 +# CVE-2026-33671 — picomatch <4.0.4 (also covers npm bundled copy above) +RUN for pkg_dir in \ + /usr/local/lib/node_modules/@modelcontextprotocol/server-github \ + /usr/local/lib/node_modules/@yoda.digital/gitlab-mcp-server \ + /usr/local/lib/node_modules/@aashari/mcp-server-atlassian-jira \ + /usr/local/lib/node_modules/@aashari/mcp-server-atlassian-confluence; do \ + [ -d "$pkg_dir" ] && \ + cd "$pkg_dir" && \ + npm install --no-audit --no-fund \ + @modelcontextprotocol/sdk@1.25.2 \ + picomatch@4.0.4 \ + || true; \ + done + # Workspace and Claude config dir — owned by the built-in node user (uid 1000). # Pre-creating ~/.claude ensures the named volume is initialised with the # correct ownership when first mounted (Docker copies image content into From b741b02408cde13fda682b5fb376926eefc250d5 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Mon, 20 Apr 2026 16:37:00 +0200 Subject: [PATCH 66/69] fix(dockerfile): scrub npm auth tokens written during image build npm automatically picks up GITHUB_TOKEN / NPM_TOKEN from the build environment and writes them as _authToken entries in /root/.npmrc and /usr/local/etc/npmrc during 'npm install -g'. Add a cleanup RUN step that removes any npmrc file containing auth tokens before the image is finalised, and explicitly deletes the two most common registry auth keys via 'npm config delete'. Also add .npmrc to .dockerignore as an extra guard against accidentally COPY-ing a local credential file into the build context. Co-Authored-By: Claude Sonnet 4.6 --- .dockerignore | 1 + claude.sh | 0 claude/Dockerfile | 9 +++++++++ hooks/pre-commit | 0 launch.sh | 0 setup.sh | 0 6 files changed, 10 insertions(+) mode change 100644 => 100755 claude.sh mode change 100644 => 100755 hooks/pre-commit mode change 100644 => 100755 launch.sh mode change 100644 => 100755 setup.sh diff --git a/.dockerignore b/.dockerignore index ba76e9d..fc75c26 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,5 @@ .env +.npmrc *.log .git README.md diff --git a/claude.sh b/claude.sh old mode 100644 new mode 100755 diff --git a/claude/Dockerfile b/claude/Dockerfile index fdcccec..9626ff5 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -62,6 +62,15 @@ RUN for pkg_dir in \ || true; \ done +# Remove any npm auth credentials written during install. +# npm automatically picks up GITHUB_TOKEN and NPM_TOKEN from the build environment +# and persists them in .npmrc files — scrub all of them before the image is finalised. +RUN find /root /home /usr/local/etc -name ".npmrc" -o -name "npmrc" \ + | xargs grep -l "_authToken\|_auth\b" 2>/dev/null \ + | xargs rm -f 2>/dev/null || true \ + && npm config delete //npm.pkg.github.com/:_authToken 2>/dev/null || true \ + && npm config delete //registry.npmjs.org/:_authToken 2>/dev/null || true + # Workspace and Claude config dir — owned by the built-in node user (uid 1000). # Pre-creating ~/.claude ensures the named volume is initialised with the # correct ownership when first mounted (Docker copies image content into diff --git a/hooks/pre-commit b/hooks/pre-commit old mode 100644 new mode 100755 diff --git a/launch.sh b/launch.sh old mode 100644 new mode 100755 diff --git a/setup.sh b/setup.sh old mode 100644 new mode 100755 From 71494a59b309d21364c82fc7dc2df78bef6c1684 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Mon, 20 Apr 2026 22:45:48 +0200 Subject: [PATCH 67/69] ci(scan): add Grype scanning alongside Trivy; fix --vuln-type flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add anchore/grype:v0.88.0 as a second vulnerability scanner in the scan job. Both images are scanned with Grype after Trivy — using --fail-on high --only-fixed to match Trivy's HIGH/CRITICAL + ignore-unfixed gate. Having two independent scanners catches CVEs that may be missing from either database. Also fix a typo in the Trivy claude scan step: --package-type is not a valid Trivy flag; corrected to --vuln-type (matching the proxy step). Co-Authored-By: Claude Sonnet 4.6 --- .forgejo/workflows/docker-build.yml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/docker-build.yml b/.forgejo/workflows/docker-build.yml index 615da18..b13214f 100644 --- a/.forgejo/workflows/docker-build.yml +++ b/.forgejo/workflows/docker-build.yml @@ -11,6 +11,7 @@ env: FORGEJO_HOST: code.zeidler.dev HELM_EXPERIMENTAL_OCI: 1 TRIVY_IMAGE: registry.zeidler.dev/docker-hub/aquasec/trivy:0.70.0 + GRYPE_IMAGE: registry.zeidler.dev/docker-hub/anchore/grype:v0.88.0 jobs: check-docker: @@ -61,7 +62,7 @@ jobs: --format cyclonedx --output /output/sbom-proxy.cdx.json \ scan/proxy:latest - - name: Scan proxy image + - name: Scan proxy image (Trivy) run: | docker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ @@ -71,6 +72,15 @@ jobs: --format table \ scan/proxy:latest + - name: Scan proxy image (Grype) + run: | + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + ${{ env.GRYPE_IMAGE }} \ + docker:scan/proxy:latest \ + --fail-on high \ + --only-fixed + - name: Build claude image for scanning run: docker build -t scan/claude:latest ./claude @@ -84,7 +94,7 @@ jobs: --format cyclonedx --output /output/sbom-claude.cdx.json \ scan/claude:latest - - name: Scan claude image + - name: Scan claude image (Trivy) run: | docker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ @@ -94,6 +104,15 @@ jobs: --format table \ scan/claude:latest + - name: Scan claude image (Grype) + run: | + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + ${{ env.GRYPE_IMAGE }} \ + docker:scan/claude:latest \ + --fail-on high \ + --only-fixed + - name: Upload SBOMs if: always() uses: actions/upload-artifact@v4 From e8d134f5a93a163f59586c9fb8bd07bf6f41c7b4 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Mon, 20 Apr 2026 23:23:27 +0200 Subject: [PATCH 68/69] fix(dockerfile): bump MCP SDK 1.26.0, patch brace-expansion 5.0.5 (GHSA-345p-7cg4-v4c7, GHSA-f886-m6hf-6m8v) Add comprehensive picomatch sweep for nested node_modules; use direct tarball-copy strategy to patch brace-expansion inside npm's own bundled node_modules where npm-install --prefix cannot reach. Co-Authored-By: Claude Sonnet 4.6 --- claude/Dockerfile | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/claude/Dockerfile b/claude/Dockerfile index 9626ff5..1abcb7f 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -48,7 +48,9 @@ RUN npm install -g \ # Patch transitive CVEs bundled inside MCP server node_modules: # CVE-2025-66414, CVE-2026-0621 — @modelcontextprotocol/sdk <1.25.2 -# CVE-2026-33671 — picomatch <4.0.4 (also covers npm bundled copy above) +# GHSA-345p-7cg4-v4c7 — @modelcontextprotocol/sdk <1.26.0 +# CVE-2026-33671 — picomatch <4.0.4 (also covers npm bundled copy above) +# GHSA-f886-m6hf-6m8v — brace-expansion <5.0.5 RUN for pkg_dir in \ /usr/local/lib/node_modules/@modelcontextprotocol/server-github \ /usr/local/lib/node_modules/@yoda.digital/gitlab-mcp-server \ @@ -57,10 +59,28 @@ RUN for pkg_dir in \ [ -d "$pkg_dir" ] && \ cd "$pkg_dir" && \ npm install --no-audit --no-fund \ - @modelcontextprotocol/sdk@1.25.2 \ + @modelcontextprotocol/sdk@1.26.0 \ picomatch@4.0.4 \ + brace-expansion@5.0.5 \ || true; \ - done + done \ + && find /usr/local/lib/node_modules -name "picomatch" -type d | while read dir; do \ + ver=$(node -p "require('$dir/package.json').version" 2>/dev/null); \ + [ "$ver" = "4.0.3" ] || continue; \ + prefix=$(dirname "$(dirname "$dir")"); \ + npm install --prefix "$prefix" picomatch@4.0.4 \ + --no-save --no-audit --no-fund 2>/dev/null || true; \ + done \ + && cd /tmp \ + && npm pack brace-expansion@5.0.5 --no-audit 2>/dev/null \ + && tar xzf brace-expansion-5.0.5.tgz \ + && find /usr/local/lib/node_modules -name "package.json" -path "*/brace-expansion/package.json" \ + | xargs grep -l '"version": "5.0.4"' 2>/dev/null \ + | while read pj; do \ + echo "Patching brace-expansion at $(dirname "$pj")"; \ + cp -r /tmp/package/. "$(dirname "$pj")/"; \ + done \ + && rm -rf /tmp/brace-expansion-5.0.5.tgz /tmp/package # Remove any npm auth credentials written during install. # npm automatically picks up GITHUB_TOKEN and NPM_TOKEN from the build environment From 94333e4d325b5c1f925e817542dbac1c347bd28c Mon Sep 17 00:00:00 2001 From: docker-claude Date: Mon, 20 Apr 2026 23:32:26 +0200 Subject: [PATCH 69/69] fix(dockerfile): purge npm cache in same layer as installs to prevent secret leakage Consolidate MCP install, CVE patches, .npmrc scrub, and npm cache clean into a single RUN so the download cache (which contains package tarballs with example GitHub tokens) is never committed to a layer. Co-Authored-By: Claude Sonnet 4.6 --- claude/Dockerfile | 73 +++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/claude/Dockerfile b/claude/Dockerfile index 1abcb7f..6d6c0fd 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -39,57 +39,54 @@ COPY settings.json /etc/claude-code/managed-settings.json # Install Claude Code stable release RUN curl -fsSL https://claude.ai/install.sh | bash -s stable -# Install MCP servers globally — entry points land in /usr/local/lib/node_modules/ -RUN npm install -g \ - @modelcontextprotocol/server-github \ - @yoda.digital/gitlab-mcp-server \ - @aashari/mcp-server-atlassian-jira \ - @aashari/mcp-server-atlassian-confluence - -# Patch transitive CVEs bundled inside MCP server node_modules: +# Install MCP servers, patch transitive CVEs, scrub credentials and cache — all in one +# layer so nothing is committed to the image between install and cleanup. +# +# CVEs patched: # CVE-2025-66414, CVE-2026-0621 — @modelcontextprotocol/sdk <1.25.2 # GHSA-345p-7cg4-v4c7 — @modelcontextprotocol/sdk <1.26.0 -# CVE-2026-33671 — picomatch <4.0.4 (also covers npm bundled copy above) +# CVE-2026-33671 — picomatch <4.0.4 # GHSA-f886-m6hf-6m8v — brace-expansion <5.0.5 -RUN for pkg_dir in \ - /usr/local/lib/node_modules/@modelcontextprotocol/server-github \ - /usr/local/lib/node_modules/@yoda.digital/gitlab-mcp-server \ - /usr/local/lib/node_modules/@aashari/mcp-server-atlassian-jira \ - /usr/local/lib/node_modules/@aashari/mcp-server-atlassian-confluence; do \ - [ -d "$pkg_dir" ] && \ - cd "$pkg_dir" && \ - npm install --no-audit --no-fund \ - @modelcontextprotocol/sdk@1.26.0 \ - picomatch@4.0.4 \ - brace-expansion@5.0.5 \ - || true; \ - done \ +RUN npm install -g \ + @modelcontextprotocol/server-github \ + @yoda.digital/gitlab-mcp-server \ + @aashari/mcp-server-atlassian-jira \ + @aashari/mcp-server-atlassian-confluence \ + && for pkg_dir in \ + /usr/local/lib/node_modules/@modelcontextprotocol/server-github \ + /usr/local/lib/node_modules/@yoda.digital/gitlab-mcp-server \ + /usr/local/lib/node_modules/@aashari/mcp-server-atlassian-jira \ + /usr/local/lib/node_modules/@aashari/mcp-server-atlassian-confluence; do \ + [ -d "$pkg_dir" ] && \ + cd "$pkg_dir" && \ + npm install --no-audit --no-fund \ + @modelcontextprotocol/sdk@1.26.0 \ + picomatch@4.0.4 \ + brace-expansion@5.0.5 \ + || true; \ + done \ && find /usr/local/lib/node_modules -name "picomatch" -type d | while read dir; do \ - ver=$(node -p "require('$dir/package.json').version" 2>/dev/null); \ - [ "$ver" = "4.0.3" ] || continue; \ - prefix=$(dirname "$(dirname "$dir")"); \ - npm install --prefix "$prefix" picomatch@4.0.4 \ - --no-save --no-audit --no-fund 2>/dev/null || true; \ - done \ + ver=$(node -p "require('$dir/package.json').version" 2>/dev/null); \ + [ "$ver" = "4.0.3" ] || continue; \ + prefix=$(dirname "$(dirname "$dir")"); \ + npm install --prefix "$prefix" picomatch@4.0.4 \ + --no-save --no-audit --no-fund 2>/dev/null || true; \ + done \ && cd /tmp \ && npm pack brace-expansion@5.0.5 --no-audit 2>/dev/null \ && tar xzf brace-expansion-5.0.5.tgz \ && find /usr/local/lib/node_modules -name "package.json" -path "*/brace-expansion/package.json" \ | xargs grep -l '"version": "5.0.4"' 2>/dev/null \ | while read pj; do \ - echo "Patching brace-expansion at $(dirname "$pj")"; \ cp -r /tmp/package/. "$(dirname "$pj")/"; \ done \ - && rm -rf /tmp/brace-expansion-5.0.5.tgz /tmp/package - -# Remove any npm auth credentials written during install. -# npm automatically picks up GITHUB_TOKEN and NPM_TOKEN from the build environment -# and persists them in .npmrc files — scrub all of them before the image is finalised. -RUN find /root /home /usr/local/etc -name ".npmrc" -o -name "npmrc" \ - | xargs grep -l "_authToken\|_auth\b" 2>/dev/null \ - | xargs rm -f 2>/dev/null || true \ + && rm -rf /tmp/brace-expansion-5.0.5.tgz /tmp/package \ + && find /root /home /usr/local/etc -name ".npmrc" -o -name "npmrc" \ + | xargs grep -l "_authToken\|_auth\b" 2>/dev/null \ + | xargs rm -f 2>/dev/null || true \ && npm config delete //npm.pkg.github.com/:_authToken 2>/dev/null || true \ - && npm config delete //registry.npmjs.org/:_authToken 2>/dev/null || true + && npm config delete //registry.npmjs.org/:_authToken 2>/dev/null || true \ + && npm cache clean --force # Workspace and Claude config dir — owned by the built-in node user (uid 1000). # Pre-creating ~/.claude ensures the named volume is initialised with the