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.
This commit is contained in:
docker-claude 2026-04-14 22:40:57 +02:00
parent 782370e014
commit 88805a3c24
9 changed files with 53 additions and 57 deletions

View file

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

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

View file

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

0
claude.sh Normal file → Executable file
View file

View file

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

View file

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

19
proxy/Dockerfile Normal file
View file

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

View file

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