From 88805a3c243ac98f4c4ef0ebd31f85bfaceafcca Mon Sep 17 00:00:00 2001 From: docker-claude Date: Tue, 14 Apr 2026 22:40:57 +0200 Subject: [PATCH] refactor(docker): migrate both images to Alpine Replace node:20-slim/ubuntu:22.04 with node:20-alpine/alpine:3.21. Switch package management from apt to apk (--no-cache, no cleanup layer). Use Alpine addgroup/adduser in claude/Dockerfile. Update proxy to use squid user (Alpine convention) and /var/cache/squid cache path. Fix proxy/Dockerfile COPY path now that context is proxy/. Move webui-entrypoint.sh into claude/ to match its build context. Fix docker-compose.yml webui context to claude/, update proxy tmpfs path. --- CLAUDE.md | 18 +++++++------ Dockerfile.proxy | 25 ------------------- README.md | 8 +++--- claude.sh | 0 Dockerfile.claude => claude/Dockerfile | 13 +++++----- .../webui-entrypoint.sh | 0 docker-compose.yml | 23 ++++++++--------- proxy/Dockerfile | 19 ++++++++++++++ proxy/squid.conf | 4 ++- 9 files changed, 53 insertions(+), 57 deletions(-) delete mode 100644 Dockerfile.proxy mode change 100644 => 100755 claude.sh rename Dockerfile.claude => claude/Dockerfile (72%) rename webui-entrypoint.sh => claude/webui-entrypoint.sh (100%) create mode 100644 proxy/Dockerfile diff --git a/CLAUDE.md b/CLAUDE.md index 6c93531..547181c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,13 +10,13 @@ This file provides context and guidance for working with this project. Three containers managed by Docker Compose: -- **`claude`** — Claude Code CLI, non-root (UID 1000), isolated to an internal-only Docker network -- **`webui`** — Claude Code as a browser terminal (ttyd on port 7681), same image as `claude`, non-root (UID 1000), same network isolation, basic auth required -- **`proxy`** — Squid forward proxy, non-root (UID 13), bridges the internal network to the internet with an egress allowlist +- **`claude`** — Claude Code CLI (`node:20-alpine`), non-root (UID 1000), isolated to an internal-only Docker network +- **`webui`** — Claude Code as a browser terminal via ttyd (`node:20-alpine`), non-root (UID 1000), same network isolation, basic auth required +- **`proxy`** — Squid forward proxy (`alpine:3.21`), `squid` user, bridges the internal network to the internet with an egress allowlist Key Docker network property: `claude-internal` has `internal: true`, meaning Docker adds no default gateway. The `claude` and `webui` containers physically cannot reach the internet without going through the `proxy` container. -The `webui` service reuses `Dockerfile.claude`. Its entrypoint (`webui-entrypoint.sh`) starts `ttyd --credential user:pass claude` instead of `claude` directly. +The `webui` service reuses `claude/Dockerfile`. Its entrypoint (`claude/webui-entrypoint.sh`) starts `ttyd --credential user:pass claude` instead of `claude` directly. ## File Structure @@ -24,10 +24,11 @@ The `webui` service reuses `Dockerfile.claude`. Its entrypoint (`webui-entrypoin docker-claude/ ├── claude.sh # Control script: start/stop/run/web/web-stop/update/logs/status/shell ├── docker-compose.yml # Service definitions and network topology -├── Dockerfile.claude # Claude Code + ttyd container (node:20-slim, UID 1000) -├── Dockerfile.proxy # Squid proxy sidecar (ubuntu:22.04, UID 13) -├── webui-entrypoint.sh # Entrypoint for webui service: starts ttyd wrapping claude +├── claude/ +│ ├── Dockerfile # Claude Code + ttyd (node:20-alpine, UID 1000) +│ └── webui-entrypoint.sh # Entrypoint for webui: starts ttyd wrapping claude ├── proxy/ +│ ├── Dockerfile # Squid proxy sidecar (alpine:3.21, squid user) │ └── squid.conf # Squid ACL config — egress allowlist lives here ├── .env.example # Template for ANTHROPIC_API_KEY, WEBUI_PASSWORD, etc. ├── .gitignore # Excludes .env and logs @@ -48,7 +49,8 @@ cp .env.example .env # set ANTHROPIC_API_KEY (and WEBUI_PASSWORD for web mo ## Coding Standards - Shell scripts use `set -euo pipefail` -- Dockerfiles use `--no-install-recommends` and clean apt caches in the same layer +- Dockerfiles use Alpine (`node:20-alpine`, `alpine:3.21`) for minimal attack surface +- Alpine packages use `apk add --no-cache`; no apt cache cleanup layer needed - No capabilities granted; `no-new-privileges` on all containers - `.env` is never committed (enforced by `.gitignore` and `.dockerignore`) - Commit messages follow **Angular format**: `type(scope): summary` diff --git a/Dockerfile.proxy b/Dockerfile.proxy deleted file mode 100644 index cfec906..0000000 --- a/Dockerfile.proxy +++ /dev/null @@ -1,25 +0,0 @@ -FROM ubuntu:22.04 - -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update && apt-get install -y --no-install-recommends \ - squid \ - && rm -rf /var/lib/apt/lists/* - -# Give the proxy system user (UID 13) ownership of all Squid paths -RUN mkdir -p /var/spool/squid /var/log/squid \ - && chown -R proxy:proxy /var/spool/squid /var/log/squid /etc/squid - -COPY --chown=proxy:proxy proxy/squid.conf /etc/squid/squid.conf - -USER proxy - -# Initialise cache directories as the proxy user -RUN squid -N -f /etc/squid/squid.conf -z 2>/dev/null || true - -EXPOSE 3128 - -HEALTHCHECK --interval=10s --timeout=5s --retries=3 \ - CMD /bin/bash -c 'echo >/dev/tcp/127.0.0.1/3128' - -CMD ["squid", "-N", "-f", "/etc/squid/squid.conf"] diff --git a/README.md b/README.md index 1fd96a6..2da158b 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,9 @@ Runs [Claude Code](https://claude.ai/code) inside an isolated Docker environment └──────────────────────────────────────────────────────────┘ ``` -- **`claude`** — Claude Code CLI, UID 1000, on `claude-internal` only -- **`webui`** — Claude Code in a browser terminal (ttyd), UID 1000, on `claude-internal` only, port 7681 -- **`proxy`** — Squid forward proxy, UID 13, bridges `claude-internal` ↔ internet with egress allowlist +- **`claude`** — Claude Code CLI (`node:20-alpine`), UID 1000, on `claude-internal` only +- **`webui`** — Claude Code in a browser terminal via ttyd (`node:20-alpine`), UID 1000, on `claude-internal` only, port 7681 +- **`proxy`** — Squid forward proxy (`alpine:3.21`), bridges `claude-internal` ↔ internet with egress allowlist - **`claude-internal`** — `internal: true`; no default gateway, containers cannot reach the internet directly - **`proxy-external`** — Standard bridge; proxy sidecar only @@ -129,7 +129,7 @@ Rebuild after changes: | Control | claude / webui | proxy | |---|---|---| -| Non-root user | UID 1000 (`claude`) | UID 13 (`proxy`) | +| Non-root user | UID 1000 (`claude`) | `squid` user | | `no-new-privileges` | yes | yes | | All capabilities dropped | yes | yes | | Direct internet access | no (`internal` network only) | allowlisted only | diff --git a/claude.sh b/claude.sh old mode 100644 new mode 100755 diff --git a/Dockerfile.claude b/claude/Dockerfile similarity index 72% rename from Dockerfile.claude rename to claude/Dockerfile index 9757f8a..42b2cf7 100644 --- a/Dockerfile.claude +++ b/claude/Dockerfile @@ -1,20 +1,19 @@ -FROM node:20-slim +FROM node:20-alpine -# Install minimal runtime dependencies -RUN apt-get update && apt-get install -y --no-install-recommends \ +# Install runtime dependencies +RUN apk add --no-cache \ git \ curl \ ca-certificates \ bash \ - ttyd \ - && rm -rf /var/lib/apt/lists/* + ttyd # Entrypoint used by the webui service (ttyd wrapping claude) COPY --chmod=755 webui-entrypoint.sh /usr/local/bin/webui-entrypoint.sh # Create non-root user -RUN groupadd -g 1000 claude \ - && useradd -u 1000 -g claude -m -s /bin/bash claude +RUN addgroup -g 1000 claude \ + && adduser -u 1000 -G claude -s /bin/bash -D claude # Install Claude Code globally (runs as root for npm -g, then drops) RUN npm install -g @anthropic-ai/claude-code diff --git a/webui-entrypoint.sh b/claude/webui-entrypoint.sh similarity index 100% rename from webui-entrypoint.sh rename to claude/webui-entrypoint.sh diff --git a/docker-compose.yml b/docker-compose.yml index 89fa2e0..8d6c1ab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,15 +1,14 @@ services: - # ─── Proxy sidecar ───────────────────────────────────────────────────────── # Bridges the isolated internal network to the internet. # Enforces an egress allowlist — see proxy/squid.conf. proxy: build: - context: . - dockerfile: Dockerfile.proxy + context: proxy + dockerfile: Dockerfile networks: - - claude-internal # reachable by claude and webui containers - - proxy-external # has outbound internet access + - claude-internal # reachable by claude and webui containers + - proxy-external # has outbound internet access restart: unless-stopped security_opt: - no-new-privileges:true @@ -18,7 +17,7 @@ services: read_only: true tmpfs: - /tmp - - /var/spool/squid + - /var/cache/squid - /var/log/squid # ─── Claude Code CLI container ───────────────────────────────────────────── @@ -26,13 +25,13 @@ services: # Run via "docker compose run --rm claude" (managed by claude.sh). claude: build: - context: . - dockerfile: Dockerfile.claude + context: claude/ + dockerfile: Dockerfile depends_on: proxy: condition: service_healthy networks: - - claude-internal # only — no route to the internet + - claude-internal # only — no route to the internet environment: - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - HTTP_PROXY=http://proxy:3128 @@ -54,14 +53,14 @@ services: # Network isolation is identical to the CLI container. webui: build: - context: . - dockerfile: Dockerfile.claude + context: claude/ + dockerfile: Dockerfile entrypoint: ["/usr/local/bin/webui-entrypoint.sh"] depends_on: proxy: condition: service_healthy networks: - - claude-internal # only — no route to the internet + - claude-internal # only — no route to the internet environment: - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - HTTP_PROXY=http://proxy:3128 diff --git a/proxy/Dockerfile b/proxy/Dockerfile new file mode 100644 index 0000000..7382d3c --- /dev/null +++ b/proxy/Dockerfile @@ -0,0 +1,19 @@ +FROM alpine:3.21 + +# squid: proxy. netcat-openbsd: health check +RUN apk add --no-cache squid netcat-openbsd + +# squid user is created by the package (apk add squid) +RUN mkdir -p /var/cache/squid /var/log/squid \ + && chown -R squid:squid /var/cache/squid /var/log/squid /etc/squid + +COPY --chown=squid:squid squid.conf /etc/squid/squid.conf + +USER squid + +EXPOSE 3128 + +HEALTHCHECK --interval=10s --timeout=5s --retries=3 \ + CMD nc -z 127.0.0.1 3128 || exit 1 + +CMD ["squid", "-N", "-f", "/etc/squid/squid.conf"] diff --git a/proxy/squid.conf b/proxy/squid.conf index 6ef039f..9482ad1 100644 --- a/proxy/squid.conf +++ b/proxy/squid.conf @@ -14,7 +14,7 @@ cache_store_log none # ─── No disk cache ──────────────────────────────────────────────────────────── cache deny all -coredump_dir /var/spool/squid +coredump_dir /var/cache/squid # ─── ACL Definitions ────────────────────────────────────────────────────────── acl SSL_ports port 443 @@ -26,6 +26,8 @@ acl CONNECT method CONNECT # Add domains here as needed. Leading dot matches all subdomains. acl allowed_sites dstdomain api.anthropic.com acl allowed_sites dstdomain statsig.anthropic.com +acl allowed_sites dstdomain localhost +acl allowed_sites dstdomain .local # ─── Access rules ───────────────────────────────────────────────────────────── # Block requests to non-standard ports