Compare commits
No commits in common. "main" and "master" have entirely different histories.
18 changed files with 244 additions and 933 deletions
|
|
@ -1,5 +1,4 @@
|
||||||
.env
|
.env
|
||||||
.npmrc
|
|
||||||
*.log
|
*.log
|
||||||
.git
|
.git
|
||||||
README.md
|
README.md
|
||||||
|
|
|
||||||
35
.env.example
35
.env.example
|
|
@ -1,34 +1,9 @@
|
||||||
# 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.
|
||||||
|
|
||||||
# ─── Image version ────────────────────────────────────────────────────────────
|
# Required: your Anthropic API key
|
||||||
|
ANTHROPIC_API_KEY=sk-ant-...
|
||||||
|
|
||||||
# Pin to a specific image tag. Defaults to "latest" if unset.
|
# Optional: mount a host directory as /workspace inside the Claude container.
|
||||||
# IMAGE_TAG=0.1.42
|
# If unset, a named Docker volume is used (fully isolated from the host).
|
||||||
|
# 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
|
|
||||||
|
|
|
||||||
|
|
@ -1,170 +0,0 @@
|
||||||
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
|
|
||||||
43
CLAUDE.md
43
CLAUDE.md
|
|
@ -10,34 +10,22 @@ 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 (`node:24-alpine`), runs as the built-in `node` user (UID 1000), isolated to an internal-only Docker network
|
- **`claude`** — Claude Code CLI, non-root (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
|
- **`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` 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/update/logs/status/shell
|
├── claude.sh # Control script: start / stop / run / 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
|
├── docker-compose.yml # Service definitions and network topology
|
||||||
├── claude/
|
├── Dockerfile.claude # Claude Code container (node:20-slim, UID 1000)
|
||||||
│ └── Dockerfile # Claude Code stable release (node:24-alpine, UID 1000)
|
├── Dockerfile.proxy # Squid proxy sidecar (ubuntu:22.04, UID 13)
|
||||||
├── proxy/
|
├── proxy/
|
||||||
│ ├── Dockerfile # Squid proxy sidecar (alpine:3.21, squid user)
|
|
||||||
│ └── squid.conf # Squid ACL config — egress allowlist lives here
|
│ └── squid.conf # Squid ACL config — egress allowlist lives here
|
||||||
├── hooks/
|
├── .env.example # Template for ANTHROPIC_API_KEY
|
||||||
│ └── pre-commit # Enforces executable bit on shell scripts
|
|
||||||
├── .env.example # Template for credentials and options
|
|
||||||
├── .gitignore # Excludes .env and logs
|
├── .gitignore # Excludes .env and logs
|
||||||
├── .dockerignore # Keeps .env out of build context
|
├── .dockerignore # Keeps .env out of build context
|
||||||
└── README.md # User documentation
|
└── README.md # User documentation
|
||||||
|
|
@ -46,25 +34,16 @@ docker-claude/
|
||||||
## Development Workflow
|
## Development Workflow
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./setup.sh # first-time: configure Docker check + auth
|
chmod +x claude.sh
|
||||||
cd /path/to/project && ./claude.sh start # start proxy + launch Claude (pulls images, mounts CWD)
|
cp .env.example .env # set ANTHROPIC_API_KEY
|
||||||
./claude.sh update # pull latest images from registry
|
./claude.sh start # build + start proxy + launch Claude interactively
|
||||||
./build.sh # build images locally (development)
|
./claude.sh update # rebuild images (no cache) after upstream updates
|
||||||
```
|
|
||||||
|
|
||||||
## 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 Alpine (`node:24-alpine`, `alpine:3.21`) for minimal attack surface
|
- Dockerfiles use `--no-install-recommends` and clean apt caches in the same layer
|
||||||
- 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`
|
||||||
|
|
|
||||||
30
Dockerfile.claude
Normal file
30
Dockerfile.claude
Normal file
|
|
@ -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"]
|
||||||
25
Dockerfile.proxy
Normal file
25
Dockerfile.proxy
Normal file
|
|
@ -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"]
|
||||||
170
README.md
170
README.md
|
|
@ -1,176 +1,120 @@
|
||||||
# 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 access 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 reach 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: true) │ │
|
│ │ │ claude │◄─────(internal only)───► │ │
|
||||||
│ │ │ (UID 1000) │──────────────► ┌──────────┐ │ │
|
│ │ │ (UID 1000) │ │ │ │
|
||||||
│ │ └─────────────┘ │ proxy │ │ │
|
│ │ └─────────────┘ ┌──────┴──────┐ │ │
|
||||||
|
│ │ │ proxy │ │ │
|
||||||
│ │ │ (UID 13) │ │ │
|
│ │ │ (UID 13) │ │ │
|
||||||
│ │ └────┬─────┘ │ │
|
│ │ └──────┬──────┘ │ │
|
||||||
│ │ proxy-external │ │
|
│ │ proxy-external │ │
|
||||||
│ └──────────────────────────────────────────────────┘ │
|
│ └─────────────────────────────────────────────┘ │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ internet (allowlisted) │
|
│ internet (allowlisted) │
|
||||||
└──────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
- **`claude`** — Claude Code CLI (`node:24-alpine`), runs as the built-in `node` user (UID 1000), on `claude-internal` only
|
- **`claude` container** — Claude Code, runs as UID 1000, on `claude-internal` only (no internet route)
|
||||||
- **`proxy`** — Squid forward proxy (`alpine:3.21`), bridges `claude-internal` ↔ internet with egress allowlist
|
- **`proxy` container** — Squid forward proxy, runs as UID 13, bridges `claude-internal` ↔ internet, enforces egress allowlist
|
||||||
- **`claude-internal`** — `internal: true`; no default gateway, containers cannot reach the internet directly
|
- **`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; proxy sidecar only
|
- **`proxy-external`** — Standard bridge; the proxy sidecar uses this for controlled outbound access
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
A Docker runtime with Compose support. Choose a free, open-source option:
|
- Docker Engine 24+
|
||||||
|
- Docker Compose v2 plugin (`docker compose version`)
|
||||||
|
|
||||||
- **macOS:** [Rancher Desktop](https://rancherdesktop.io/) or [Colima](https://github.com/abiosoft/colima)
|
## Setup
|
||||||
- **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
|
||||||
ANTHROPIC_API_KEY=sk-ant-...
|
# 1. Clone / copy this repo
|
||||||
|
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
|
||||||
./launch.sh # folder picker → starts Claude in the selected directory
|
# Build images, start proxy, launch Claude Code interactively
|
||||||
```
|
|
||||||
|
|
||||||
### CLI / power users
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ~/myproject
|
|
||||||
./claude.sh start
|
./claude.sh start
|
||||||
|
|
||||||
./claude.sh stop # Stop and remove all containers
|
# Same as start but skips image rebuild (faster on subsequent runs)
|
||||||
./claude.sh update # Pull latest images from the registry
|
./claude.sh run
|
||||||
./claude.sh logs # Tail proxy logs
|
|
||||||
./claude.sh status # Show container status
|
# Stop and remove all containers (proxy + any running sessions)
|
||||||
./claude.sh shell # Debug bash shell in the Claude container
|
./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
|
||||||
```
|
```
|
||||||
|
|
||||||
### Windows: WSL2 + Docker Engine (alternative to Rancher Desktop)
|
### Working with host files
|
||||||
|
|
||||||
1. Install [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install): `wsl --install` in PowerShell
|
By default, Claude's workspace is a named Docker volume (`claude-secure-workspace`) — fully isolated from the host.
|
||||||
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
|
To mount a specific host directory:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build.sh # build with layer cache
|
WORKSPACE_DIR=$HOME/myproject ./claude.sh run
|
||||||
./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
|
# acl allowed_sites dstdomain api.github.com # uncomment if needed
|
||||||
# acl allowed_sites dstdomain registry.npmjs.org
|
# acl allowed_sites dstdomain registry.npmjs.org
|
||||||
```
|
```
|
||||||
|
|
||||||
Rebuild after changes:
|
Rebuild the proxy 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 | proxy |
|
| Control | Claude container | Proxy container |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Non-root user | UID 1000 (`node`, built into base image) | `squid` user |
|
| Non-root user | UID 1000 (`claude`) | UID 13 (`proxy`) |
|
||||||
| `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 | CWD mounted as `/workspace` | none |
|
| Host filesystem | no mounts by default | none |
|
||||||
| Docker socket | not mounted | not mounted |
|
| Docker socket | not mounted | not mounted |
|
||||||
|
|
|
||||||
19
build.sh
19
build.sh
|
|
@ -1,19 +0,0 @@
|
||||||
#!/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."
|
|
||||||
159
claude.sh
159
claude.sh
|
|
@ -1,12 +1,11 @@
|
||||||
#!/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 [--kube] <command> [args]
|
# Usage: ./claude.sh <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'
|
||||||
|
|
@ -14,110 +13,96 @@ 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; }
|
||||||
|
|
||||||
# ─── Helpers ──────────────────────────────────────────────────────────────────
|
# ─── Dependency check ─────────────────────────────────────────────────────────
|
||||||
check_deps() {
|
check_deps() {
|
||||||
if ! command -v docker &>/dev/null; then
|
if ! command -v docker &>/dev/null; then
|
||||||
error "Docker is not installed. Run ./setup.sh for install instructions."
|
error "Docker is not installed. https://docs.docker.com/get-docker/"
|
||||||
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 is not available. Run ./setup.sh for install instructions."
|
error "Docker Compose v2 plugin is required."
|
||||||
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
|
||||||
warn "Not set up yet. Run ./setup.sh first."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
set -a; source "$env_file"; set +a
|
set -a; source "$env_file"; set +a
|
||||||
if [[ -z "${ANTHROPIC_API_KEY:-}" && -z "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]]; then
|
fi
|
||||||
warn "No credentials found — Claude will ask you to log in via browser."
|
if [[ -z "${ANTHROPIC_API_KEY:-}" ]]; then
|
||||||
warn "A login URL will appear below. Open it to authenticate."
|
error "ANTHROPIC_API_KEY is not set."
|
||||||
warn "(To skip this prompt in future, run ./setup.sh to configure credentials.)"
|
error "Copy .env.example → .env and add your key, or export it in your shell."
|
||||||
echo ""
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Wrapper so every docker compose call uses the right file and project name.
|
# ─── 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" "$@"; }
|
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
|
|
||||||
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 ─────────────────────────────────────────────────────────────────
|
# ─── Commands ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
cmd_start() {
|
cmd_start() {
|
||||||
check_deps; load_env; build_volume_args
|
check_deps
|
||||||
info "Pulling latest images..."
|
load_env
|
||||||
dc pull
|
info "Building images..."
|
||||||
|
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..."
|
||||||
dc run --rm --service-ports "${VOLUME_ARGS[@]}" claude "$@"
|
# shellcheck disable=SC2046
|
||||||
|
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 "Pulling latest images from registry..."
|
info "Rebuilding images (no cache)..."
|
||||||
dc pull
|
dc build --no-cache
|
||||||
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
|
||||||
dc logs -f "${1:-proxy}"
|
local svc="${1:-proxy}"
|
||||||
|
dc logs -f "$svc"
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_status() {
|
cmd_status() {
|
||||||
|
|
@ -126,51 +111,45 @@ cmd_status() {
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_shell() {
|
cmd_shell() {
|
||||||
check_deps; load_env; build_volume_args
|
check_deps
|
||||||
|
load_env
|
||||||
warn "Opening debug shell inside Claude container (non-Claude entrypoint)."
|
warn "Opening debug shell inside Claude container (non-Claude entrypoint)."
|
||||||
dc run --rm --service-ports --entrypoint /bin/bash "${VOLUME_ARGS[@]}" claude
|
# shellcheck disable=SC2046
|
||||||
|
dc run --rm --entrypoint /bin/bash $(workspace_flag) claude
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_help() {
|
cmd_help() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: $(basename "$0") [--kube] <command> [args]
|
Usage: $(basename "$0") <command> [args]
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
start [args] Start proxy, launch Claude Code (CLI)
|
start [args] Build images, start proxy, launch Claude Code
|
||||||
|
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
|
||||||
|
|
||||||
Flags (before the subcommand):
|
Environment variables:
|
||||||
--kube Mount \$HOME/.kube read-only at /home/node/.kube (kubectl access)
|
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):
|
Defaults to a named Docker volume (fully isolated).
|
||||||
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:
|
||||||
cd ~/myproject && ./claude.sh start
|
./claude.sh start
|
||||||
cd ~/myproject && ./claude.sh --kube start
|
WORKSPACE_DIR=\$HOME/myproject ./claude.sh run
|
||||||
./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|run) shift; cmd_start "$@" ;;
|
start) 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 ;;
|
||||||
|
|
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
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"]
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"availableModels": ["sonnet", "opus", "haiku"],
|
|
||||||
"permissions": {
|
|
||||||
"allow": ["Bash(*)", "Edit(*)", "Write(*)"],
|
|
||||||
"deny": ["Bash(curl *)", "Read(.*env*)"],
|
|
||||||
"env": {
|
|
||||||
"CLAUDE_CODE_ENABLE_TELEMETRY": "0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
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:
|
||||||
image: registry.zeidler.dev/docker-public/playground/docker-claude-proxy:${IMAGE_TAG:-latest}
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.proxy
|
||||||
networks:
|
networks:
|
||||||
- claude-internal # reachable by claude container
|
- claude-internal # reachable by the claude container
|
||||||
- proxy-external # has outbound internet access
|
- proxy-external # has outbound internet access
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|
@ -15,45 +18,35 @@ services:
|
||||||
read_only: true
|
read_only: true
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /tmp
|
- /tmp
|
||||||
- /var/cache/squid
|
- /var/spool/squid
|
||||||
- /var/log/squid
|
- /var/log/squid
|
||||||
|
|
||||||
# ─── Claude Code CLI container ─────────────────────────────────────────────
|
# ─── Claude Code 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 --service-ports claude" (managed by claude.sh).
|
# Run via "docker compose run --rm claude" (managed by claude.sh).
|
||||||
claude:
|
claude:
|
||||||
image: registry.zeidler.dev/docker-public/playground/docker-claude-claude:${IMAGE_TAG:-latest}
|
build:
|
||||||
|
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
|
||||||
|
|
@ -64,4 +57,3 @@ 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
#!/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
55
launch.bat
|
|
@ -1,55 +0,0 @@
|
||||||
@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
62
launch.sh
|
|
@ -1,62 +0,0 @@
|
||||||
#!/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
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
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"]
|
|
||||||
|
|
@ -14,27 +14,18 @@ cache_store_log none
|
||||||
|
|
||||||
# ─── No disk cache ────────────────────────────────────────────────────────────
|
# ─── No disk cache ────────────────────────────────────────────────────────────
|
||||||
cache deny all
|
cache deny all
|
||||||
coredump_dir /var/cache/squid
|
coredump_dir /var/spool/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
148
setup.sh
|
|
@ -1,148 +0,0 @@
|
||||||
#!/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."
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue