Compare commits

..

No commits in common. "master" and "main" have entirely different histories.
master ... main

18 changed files with 932 additions and 243 deletions

View file

@ -1,4 +1,5 @@
.env .env
.npmrc
*.log *.log
.git .git
README.md README.md

View file

@ -1,9 +1,34 @@
# Copy this file to .env and fill in your values. # Copy this file to .env and fill in your values.
# .env is git-ignored — never commit it. # .env is git-ignored — never commit it.
# Required: your Anthropic API key # ─── Image version ────────────────────────────────────────────────────────────
ANTHROPIC_API_KEY=sk-ant-...
# Optional: mount a host directory as /workspace inside the Claude container. # Pin to a specific image tag. Defaults to "latest" if unset.
# If unset, a named Docker volume is used (fully isolated from the host). # IMAGE_TAG=0.1.42
# WORKSPACE_DIR=/absolute/path/to/your/project
# ─── 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 <sandbox-name> --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

View file

@ -0,0 +1,170 @@
name: Build images
on:
push:
branches:
- main
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
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:
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: 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
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: Generate proxy SBOM
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 (Trivy)
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: 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
- name: Generate claude SBOM
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 (Trivy)
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: 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
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
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: Login to the registry
uses: docker/login-action@v3
with:
registry: ${{ vars.REGISTRY_URL }}
username: ${{ vars.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker-container
- name: Docker publish proxy
uses: docker/build-push-action@v6
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 }}
${{ vars.REGISTRY_URL }}/docker-public/${{ env.GITHUB_REPOSITORY }}-proxy:latest
- name: Docker publish claude
uses: docker/build-push-action@v6
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 }}
${{ vars.REGISTRY_URL }}/docker-public/${{ env.GITHUB_REPOSITORY }}-claude:latest

View file

@ -10,40 +10,61 @@ This file provides context and guidance for working with this project.
Two containers managed by Docker Compose: Two containers managed by Docker Compose:
- **`claude`** — Claude Code CLI, non-root (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, non-root (UID 13), bridges the internal network to the internet with an egress allowlist - **`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. 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 `~/.claude` on the host.
## File Structure ## File Structure
``` ```
docker-claude/ docker-claude/
├── claude.sh # Control script: start / stop / run / update / logs / status / shell ├── claude.sh # Control script: start/stop/update/logs/status/shell
├── docker-compose.yml # Service definitions and network topology ├── setup.sh # First-time setup wizard (Docker check + auth config)
├── Dockerfile.claude # Claude Code container (node:20-slim, UID 1000) ├── launch.sh # Folder-picker launcher for macOS/Linux
├── Dockerfile.proxy # Squid proxy sidecar (ubuntu:22.04, UID 13) ├── 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 stable release (node:24-alpine, UID 1000)
├── proxy/ ├── proxy/
│ └── squid.conf # Squid ACL config — egress allowlist lives here │ ├── Dockerfile # Squid proxy sidecar (alpine:3.21, squid user)
├── .env.example # Template for ANTHROPIC_API_KEY │ └── squid.conf # Squid ACL config — egress allowlist lives here
├── .gitignore # Excludes .env and logs ├── hooks/
├── .dockerignore # Keeps .env out of build context │ └── pre-commit # Enforces executable bit on shell scripts
└── README.md # User documentation ├── .env.example # Template for credentials and options
├── .gitignore # Excludes .env and logs
├── .dockerignore # Keeps .env out of build context
└── README.md # User documentation
``` ```
## Development Workflow ## Development Workflow
```bash ```bash
chmod +x claude.sh ./setup.sh # first-time: configure Docker check + auth
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 start # build + start proxy + launch Claude interactively ./claude.sh update # pull latest images from registry
./claude.sh update # rebuild images (no cache) after upstream updates ./build.sh # build images locally (development)
```
## Git Hooks
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
``` ```
## Coding Standards ## Coding Standards
- Shell scripts use `set -euo pipefail` - Shell scripts use `set -euo pipefail`
- Dockerfiles use `--no-install-recommends` and clean apt caches in the same layer - 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 - No capabilities granted; `no-new-privileges` on all containers
- `.env` is never committed (enforced by `.gitignore` and `.dockerignore`) - `.env` is never committed (enforced by `.gitignore` and `.dockerignore`)
- Commit messages follow **Angular format**: `type(scope): summary` - Commit messages follow **Angular format**: `type(scope): summary`

View file

@ -1,30 +0,0 @@
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"]

View file

@ -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"]

196
README.md
View file

@ -1,120 +1,176 @@
# docker-claude # 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.
## Quick Start
**1. Install a Docker runtime**
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**
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 ## Architecture
``` ```
┌─────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────┐
│ Host machine │ │ Host machine │
│ │ │ │
│ claude.sh (control script) │ │ claude.sh (control script) │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ ┌─────────────────────────────────────────────┐ │ │ ┌──────────────────────────────────────────────────┐ │
│ │ Docker: claude-secure │ │ │ │ Docker: claude-secure │ │
│ │ │ │ │ │ │ │
│ │ ┌─────────────┐ claude-internal │ │ │ │ ┌─────────────┐ claude-internal │ │
│ │ │ claude │◄─────(internal only)───► │ │ │ │ │ claude │ (internal: true) │ │
│ │ │ (UID 1000) │ │ │ │ │ │ │ (UID 1000) │──────────────► ┌──────────┐ │ │
│ │ └─────────────┘ ┌──────┴──────┐ │ │ │ │ └─────────────┘ │ proxy │ │ │
│ │ │ proxy │ │ │ │ │ │ (UID 13) │ │ │
│ │ │ (UID 13) │ │ │ │ │ └────┬─────┘ │ │
│ │ └──────┬──────┘ │ │ │ │ proxy-external │ │
│ │ proxy-external │ │ │ └──────────────────────────────────────────────────┘ │
│ └─────────────────────────────────────────────┘ │ │ │ │
│ │ │ │ ▼ │
│ ▼ │ │ internet (allowlisted) │
│ internet (allowlisted) │ └──────────────────────────────────────────────────────────┘
└─────────────────────────────────────────────────────┘
``` ```
- **`claude` container** — Claude Code, runs as UID 1000, on `claude-internal` only (no internet route) - **`claude`** — Claude Code CLI (`node:24-alpine`), runs as the built-in `node` user (UID 1000), on `claude-internal` only
- **`proxy` container** — Squid forward proxy, runs as UID 13, bridges `claude-internal` ↔ internet, enforces egress allowlist - **`proxy`** — Squid forward proxy (`alpine:3.21`), bridges `claude-internal` ↔ internet with egress allowlist
- **`claude-internal`** — Docker bridge with `internal: true`; Docker adds no default gateway, so containers on this network cannot reach the internet directly - **`claude-internal`** — `internal: true`; no default gateway, containers cannot reach the internet directly
- **`proxy-external`** — Standard bridge; the proxy sidecar uses this for controlled outbound access - **`proxy-external`** — Standard bridge; proxy sidecar only
## Prerequisites ## Prerequisites
- Docker Engine 24+ A Docker runtime with Compose support. Choose a free, open-source option:
- Docker Compose v2 plugin (`docker compose version`)
## Setup - **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
Three options — `./setup.sh` will guide you through picking one:
### Option 1 — API key
```bash ```bash
# 1. Clone / copy this repo ANTHROPIC_API_KEY=sk-ant-...
git clone <repo> 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
``` ```
Get a key at [console.anthropic.com](https://console.anthropic.com/settings/keys).
### 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 paste the token into setup, or set it manually 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.
## Usage ## Usage
### Normal use
```bash ```bash
# Build images, start proxy, launch Claude Code interactively ./launch.sh # folder picker → starts Claude in the selected directory
```
### CLI / power users
```bash
cd ~/myproject
./claude.sh start ./claude.sh start
# Same as start but skips image rebuild (faster on subsequent runs) ./claude.sh stop # Stop and remove all containers
./claude.sh run ./claude.sh update # Pull latest images from the registry
./claude.sh logs # Tail proxy logs
# Stop and remove all containers (proxy + any running sessions) ./claude.sh status # Show container status
./claude.sh stop ./claude.sh shell # Debug bash shell in the Claude container
# 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 ### Windows: WSL2 + Docker Engine (alternative to Rancher Desktop)
By default, Claude's workspace is a named Docker volume (`claude-secure-workspace`) — fully isolated from the host. 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.
To mount a specific host directory: ### Building locally
```bash ```bash
WORKSPACE_DIR=$HOME/myproject ./claude.sh run ./build.sh # build with layer cache
./build.sh --no-cache # force full rebuild
``` ```
The directory is mounted at `/workspace` inside the container.
## Egress allowlist ## Egress allowlist
Edit `proxy/squid.conf` and add domains to the `allowed_sites` ACL: Edit `proxy/squid.conf` and add domains to the `allowed_sites` ACL:
```squid ```
acl allowed_sites dstdomain api.anthropic.com acl allowed_sites dstdomain api.anthropic.com
acl allowed_sites dstdomain statsig.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 # acl allowed_sites dstdomain registry.npmjs.org
``` ```
Rebuild the proxy after changes: Rebuild after changes:
```bash ```bash
docker compose -p claude-secure build proxy
./claude.sh stop && ./claude.sh start ./claude.sh stop && ./claude.sh start
``` ```
## Security controls ## Security controls
| Control | Claude container | Proxy container | | Control | claude | proxy |
|---|---|---| |---|---|---|
| Non-root user | UID 1000 (`claude`) | UID 13 (`proxy`) | | Non-root user | UID 1000 (`node`, built into base image) | `squid` user |
| `no-new-privileges` | yes | yes | | `no-new-privileges` | yes | yes |
| All capabilities dropped | yes | yes | | All capabilities dropped | yes | yes |
| Direct internet access | no (`internal` network only) | allowlisted only | | Direct internet access | no (`internal` network only) | allowlisted only |
| Host filesystem | no mounts by default | none | | Host filesystem | CWD mounted as `/workspace` | none |
| Docker socket | not mounted | not mounted | | Docker socket | not mounted | not mounted |

19
build.sh Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env bash
# build.sh — Build Docker images locally for development
# Usage: ./build.sh [docker build flags, e.g. --no-cache]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REGISTRY="registry.zeidler.dev/docker-public/playground"
TAG="${IMAGE_TAG:-latest}"
GREEN='\033[0;32m'; NC='\033[0m'
info() { echo -e "${GREEN}[+]${NC} $*"; }
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 "Done. Run './claude.sh start' to launch."

181
claude.sh
View file

@ -1,11 +1,12 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# claude.sh — Manage the isolated Claude Code Docker environment # claude.sh — Manage the isolated Claude Code Docker environment
# Usage: ./claude.sh <command> [args] # Usage: ./claude.sh [--kube] <command> [args]
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml" COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml"
PROJECT="claude-secure" PROJECT="claude-secure"
ALLOW_KUBE=0
# ─── Colours ────────────────────────────────────────────────────────────────── # ─── Colours ──────────────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
@ -13,96 +14,110 @@ info() { echo -e "${GREEN}[+]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; } warn() { echo -e "${YELLOW}[!]${NC} $*"; }
error() { echo -e "${RED}[-]${NC} $*" >&2; } error() { echo -e "${RED}[-]${NC} $*" >&2; }
# ─── Dependency check ───────────────────────────────────────────────────────── # ─── Helpers ──────────────────────────────────────────────────────────────────
check_deps() { check_deps() {
if ! command -v docker &>/dev/null; then if ! command -v docker &>/dev/null; then
error "Docker is not installed. https://docs.docker.com/get-docker/" 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. Start your Docker runtime, then try again."
exit 1 exit 1
fi fi
if ! docker compose version &>/dev/null 2>&1; then if ! docker compose version &>/dev/null 2>&1; then
error "Docker Compose v2 plugin is required." error "Docker Compose is not available. Run ./setup.sh for install instructions."
exit 1 exit 1
fi fi
} }
# ─── Environment loading ──────────────────────────────────────────────────────
load_env() { load_env() {
local env_file="$SCRIPT_DIR/.env" local env_file="$SCRIPT_DIR/.env"
if [[ -f "$env_file" ]]; then if [[ ! -f "$env_file" ]]; then
# shellcheck disable=SC1090 warn "Not set up yet. Run ./setup.sh first."
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 exit 1
fi fi
} # shellcheck disable=SC1090
set -a; source "$env_file"; set +a
# ─── Workspace volume resolution ────────────────────────────────────────────── if [[ -z "${ANTHROPIC_API_KEY:-}" && -z "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]]; then
# Default: named Docker volume (fully isolated). warn "No credentials found — Claude will ask you to log in via browser."
# Override: export WORKSPACE_DIR=/path/to/project before running. warn "A login URL will appear below. Open it to authenticate."
workspace_flag() { warn "(To skip this prompt in future, run ./setup.sh to configure credentials.)"
if [[ -n "${WORKSPACE_DIR:-}" ]]; then echo ""
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 fi
} }
# ─── Compose wrapper ────────────────────────────────────────────────────────── # Wrapper so every docker compose call uses the right file and project name.
dc() { docker compose -f "$COMPOSE_FILE" -p "$PROJECT" "$@"; } dc() { docker compose -f "$COMPOSE_FILE" -p "$PROJECT" "$@"; }
# ─── Commands ───────────────────────────────────────────────────────────────── # ─── 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
local -a exact_blocked=( / "$HOME" /root /home )
for dir in "${exact_blocked[@]}"; do
[[ "$cwd" == "$dir" ]] && {
error "Refusing to mount $cwd as workspace — too broad. cd into a project subdirectory first."
exit 1
}
done
# 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
}
# 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
[[ "$cwd" == "$dir" || "$cwd" == "$dir/"* ]] && {
error "Refusing to mount $cwd as workspace — contains sensitive data. cd into a project subdirectory first."
exit 1
}
done
VOLUME_ARGS=("--volume" "${cwd}:/workspace:z")
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
}
# ─── Commands ─────────────────────────────────────────────────────────────────
cmd_start() { cmd_start() {
check_deps check_deps; load_env; build_volume_args
load_env info "Pulling latest images..."
info "Building images..." dc pull
dc build
info "Starting proxy sidecar..." info "Starting proxy sidecar..."
dc up -d proxy 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..." info "Launching Claude Code..."
# shellcheck disable=SC2046 dc run --rm --service-ports "${VOLUME_ARGS[@]}" claude "$@"
dc run --rm $(workspace_flag) claude "$@"
} }
cmd_stop() { cmd_stop() {
check_deps check_deps
info "Stopping all containers..." info "Stopping all containers..."
dc down 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() { cmd_update() {
check_deps check_deps
info "Rebuilding images (no cache)..." info "Pulling latest images from registry..."
dc build --no-cache dc pull
info "Update complete. Run './claude.sh start' to launch." info "Update complete. Run './claude.sh start' to launch."
} }
cmd_logs() { cmd_logs() {
check_deps check_deps
local svc="${1:-proxy}" dc logs -f "${1:-proxy}"
dc logs -f "$svc"
} }
cmd_status() { cmd_status() {
@ -111,50 +126,56 @@ cmd_status() {
} }
cmd_shell() { cmd_shell() {
check_deps check_deps; load_env; build_volume_args
load_env
warn "Opening debug shell inside Claude container (non-Claude entrypoint)." warn "Opening debug shell inside Claude container (non-Claude entrypoint)."
# shellcheck disable=SC2046 dc run --rm --service-ports --entrypoint /bin/bash "${VOLUME_ARGS[@]}" claude
dc run --rm --entrypoint /bin/bash $(workspace_flag) claude
} }
cmd_help() { cmd_help() {
cat <<EOF cat <<EOF
Usage: $(basename "$0") <command> [args] Usage: $(basename "$0") [--kube] <command> [args]
Commands: Commands:
start [args] Build images, start proxy, launch Claude Code start [args] Start proxy, launch Claude Code (CLI)
run [args] Start proxy if needed, launch Claude Code stop Stop and remove all containers
stop Stop and remove all containers update Pull latest images from the registry
update Rebuild images without cache logs [svc] Tail logs (default: proxy)
logs [svc] Tail logs (default: proxy) status Show container status
status Show container status shell Open a bash shell in the Claude container (debug)
shell Open a bash shell in the Claude container (debug) help Show this message
help Show this message
Environment variables: Flags (before the subcommand):
ANTHROPIC_API_KEY Required. Set in .env or exported in your shell. --kube Mount \$HOME/.kube read-only at /home/node/.kube (kubectl access)
WORKSPACE_DIR Optional. Absolute path to mount as /workspace.
Defaults to a named Docker volume (fully isolated). 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)
Examples: Examples:
./claude.sh start cd ~/myproject && ./claude.sh start
WORKSPACE_DIR=\$HOME/myproject ./claude.sh run cd ~/myproject && ./claude.sh --kube start
./claude.sh logs proxy ./claude.sh logs proxy
./claude.sh shell ./claude.sh shell
EOF EOF
} }
# ─── Dispatch ───────────────────────────────────────────────────────────────── # ─── Dispatch ─────────────────────────────────────────────────────────────────
while [[ "${1:-}" == --* ]]; do
case "$1" in
--kube) ALLOW_KUBE=1; shift ;;
*) break ;;
esac
done
case "${1:-help}" in case "${1:-help}" in
start) shift; cmd_start "$@" ;; start|run) shift; cmd_start "$@" ;;
stop) cmd_stop ;; stop) cmd_stop ;;
run) shift; cmd_run "$@" ;; update) cmd_update ;;
update) cmd_update ;; logs) shift; cmd_logs "${1:-}" ;;
logs) shift; cmd_logs "${1:-}" ;; status) cmd_status ;;
status) cmd_status ;; shell) cmd_shell ;;
shell) cmd_shell ;; help|-h|--help) cmd_help ;;
help|-h|--help) cmd_help ;;
*) *)
error "Unknown command: ${1}" error "Unknown command: ${1}"
cmd_help cmd_help

107
claude/Dockerfile Normal file
View file

@ -0,0 +1,107 @@
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
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 \
curl \
ca-certificates \
bash
# 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
# 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
# Install Claude Code stable release
RUN curl -fsSL https://claude.ai/install.sh | bash -s stable
# 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
# GHSA-f886-m6hf-6m8v — brace-expansion <5.0.5
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 \
&& 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 \
cp -r /tmp/package/. "$(dirname "$pj")/"; \
done \
&& 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 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
# 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
USER node
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"]

10
claude/settings.json Normal file
View file

@ -0,0 +1,10 @@
{
"availableModels": ["sonnet", "opus", "haiku"],
"permissions": {
"allow": ["Bash(*)", "Edit(*)", "Write(*)"],
"deny": ["Bash(curl *)", "Read(.*env*)"],
"env": {
"CLAUDE_CODE_ENABLE_TELEMETRY": "0"
}
}
}

View file

@ -1,15 +1,12 @@
services: services:
# ─── Proxy sidecar ───────────────────────────────────────────────────────── # ─── Proxy sidecar ─────────────────────────────────────────────────────────
# Bridges the isolated internal network to the internet. # Bridges the isolated internal network to the internet.
# Enforces an egress allowlist — see proxy/squid.conf. # Enforces an egress allowlist — see proxy/squid.conf.
proxy: proxy:
build: image: registry.zeidler.dev/docker-public/playground/docker-claude-proxy:${IMAGE_TAG:-latest}
context: .
dockerfile: Dockerfile.proxy
networks: networks:
- claude-internal # reachable by the claude container - claude-internal # reachable by claude container
- proxy-external # has outbound internet access - proxy-external # has outbound internet access
restart: unless-stopped restart: unless-stopped
security_opt: security_opt:
- no-new-privileges:true - no-new-privileges:true
@ -18,35 +15,45 @@ services:
read_only: true read_only: true
tmpfs: tmpfs:
- /tmp - /tmp
- /var/spool/squid - /var/cache/squid
- /var/log/squid - /var/log/squid
# ─── Claude Code container ───────────────────────────────────────────────── # ─── Claude Code CLI container ─────────────────────────────────────────────
# No direct internet access. All egress routes through the proxy sidecar. # 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: claude:
build: image: registry.zeidler.dev/docker-public/playground/docker-claude-claude:${IMAGE_TAG:-latest}
context: .
dockerfile: Dockerfile.claude
depends_on: depends_on:
proxy: proxy:
condition: service_healthy condition: service_healthy
networks: networks:
- claude-internal # only — no route to the internet - claude-internal # only — no route to the internet
environment: 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 - HTTP_PROXY=http://proxy:3128
- HTTPS_PROXY=http://proxy:3128 - HTTPS_PROXY=http://proxy:3128
- ALL_PROXY=http://proxy:3128 - ALL_PROXY=http://proxy:3128
- NO_PROXY=localhost,127.0.0.1 - 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"
volumes:
- ${HOME}/.claude:/home/node/.claude
# Workspace is injected by claude.sh via --volume flag at run time (current directory).
security_opt: security_opt:
- no-new-privileges:true - no-new-privileges:true
cap_drop: cap_drop:
- ALL - ALL
stdin_open: true stdin_open: true
tty: 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: networks:
# Internal-only: Docker adds no default gateway → no direct internet route # Internal-only: Docker adds no default gateway → no direct internet route
@ -57,3 +64,4 @@ networks:
# External: standard bridge with internet access (proxy only) # External: standard bridge with internet access (proxy only)
proxy-external: proxy-external:
driver: bridge driver: bridge

13
hooks/pre-commit Executable file
View file

@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Ensure control scripts stay executable.
set -euo pipefail
SCRIPTS=(claude.sh build.sh setup.sh launch.sh hooks/pre-commit)
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

55
launch.bat Normal file
View file

@ -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

62
launch.sh Executable file
View file

@ -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

19
proxy/Dockerfile Normal file
View file

@ -0,0 +1,19 @@
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
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"]

View file

@ -14,18 +14,27 @@ cache_store_log none
# ─── No disk cache ──────────────────────────────────────────────────────────── # ─── No disk cache ────────────────────────────────────────────────────────────
cache deny all cache deny all
coredump_dir /var/spool/squid coredump_dir /var/cache/squid
# ─── ACL Definitions ────────────────────────────────────────────────────────── # ─── ACL Definitions ──────────────────────────────────────────────────────────
acl SSL_ports port 443 acl SSL_ports port 443
acl SSL_ports port 6443 # Kubernetes API server
acl Safe_ports port 80 acl Safe_ports port 80
acl Safe_ports port 443 acl Safe_ports port 443
acl Safe_ports port 6443 # Kubernetes API server
acl CONNECT method CONNECT acl CONNECT method CONNECT
# ─── Egress allowlist ───────────────────────────────────────────────────────── # ─── Egress allowlist ─────────────────────────────────────────────────────────
# Add domains here as needed. Leading dot matches all subdomains. # Add domains here as needed. Leading dot matches all subdomains.
acl allowed_sites dstdomain api.anthropic.com acl allowed_sites dstdomain api.anthropic.com
acl allowed_sites dstdomain statsig.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
# ─── Access rules ───────────────────────────────────────────────────────────── # ─── Access rules ─────────────────────────────────────────────────────────────
# Block requests to non-standard ports # Block requests to non-standard ports

148
setup.sh Executable file
View file

@ -0,0 +1,148 @@
#!/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}"; }
# ─── 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."
docker_install_hint
exit 1
fi
if ! docker info &>/dev/null 2>&1; then
error "Docker is installed but not running."
docker_not_running_hint
exit 1
fi
if ! docker compose version &>/dev/null 2>&1; then
error "Docker Compose is not available."
echo " Docker Compose is included with Rancher Desktop and Docker Engine."
docker_install_hint
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."