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:
parent
782370e014
commit
88805a3c24
9 changed files with 53 additions and 57 deletions
18
CLAUDE.md
18
CLAUDE.md
|
|
@ -10,13 +10,13 @@ This file provides context and guidance for working with this project.
|
||||||
|
|
||||||
Three containers managed by Docker Compose:
|
Three containers managed by Docker Compose:
|
||||||
|
|
||||||
- **`claude`** — Claude Code CLI, non-root (UID 1000), isolated to an internal-only Docker network
|
- **`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 (ttyd on port 7681), same image as `claude`, non-root (UID 1000), same network isolation, basic auth required
|
- **`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, 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` and `webui` containers physically cannot reach the internet without going through the `proxy` container.
|
Key Docker network property: `claude-internal` has `internal: true`, meaning Docker adds no default gateway. The `claude` 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
|
## File Structure
|
||||||
|
|
||||||
|
|
@ -24,10 +24,11 @@ The `webui` service reuses `Dockerfile.claude`. Its entrypoint (`webui-entrypoin
|
||||||
docker-claude/
|
docker-claude/
|
||||||
├── claude.sh # Control script: start/stop/run/web/web-stop/update/logs/status/shell
|
├── claude.sh # Control script: start/stop/run/web/web-stop/update/logs/status/shell
|
||||||
├── docker-compose.yml # Service definitions and network topology
|
├── docker-compose.yml # Service definitions and network topology
|
||||||
├── Dockerfile.claude # Claude Code + ttyd container (node:20-slim, UID 1000)
|
├── claude/
|
||||||
├── Dockerfile.proxy # Squid proxy sidecar (ubuntu:22.04, UID 13)
|
│ ├── Dockerfile # Claude Code + ttyd (node:20-alpine, UID 1000)
|
||||||
├── webui-entrypoint.sh # Entrypoint for webui service: starts ttyd wrapping claude
|
│ └── webui-entrypoint.sh # Entrypoint for webui: starts ttyd wrapping claude
|
||||||
├── 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
|
||||||
├── .env.example # Template for ANTHROPIC_API_KEY, WEBUI_PASSWORD, etc.
|
├── .env.example # Template for ANTHROPIC_API_KEY, WEBUI_PASSWORD, etc.
|
||||||
├── .gitignore # Excludes .env and logs
|
├── .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
|
## 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: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
|
- 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`
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
|
||||||
|
|
@ -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
|
- **`claude`** — Claude Code CLI (`node:20-alpine`), UID 1000, on `claude-internal` only
|
||||||
- **`webui`** — Claude Code in a browser terminal (ttyd), UID 1000, on `claude-internal` only, port 7681
|
- **`webui`** — Claude Code in a browser terminal via ttyd (`node:20-alpine`), UID 1000, on `claude-internal` only, port 7681
|
||||||
- **`proxy`** — Squid forward proxy, UID 13, bridges `claude-internal` ↔ internet with egress allowlist
|
- **`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
|
- **`claude-internal`** — `internal: true`; no default gateway, containers cannot reach the internet directly
|
||||||
- **`proxy-external`** — Standard bridge; proxy sidecar only
|
- **`proxy-external`** — Standard bridge; proxy sidecar only
|
||||||
|
|
||||||
|
|
@ -129,7 +129,7 @@ Rebuild after changes:
|
||||||
|
|
||||||
| Control | claude / webui | proxy |
|
| 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 |
|
| `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 |
|
||||||
|
|
|
||||||
0
claude.sh
Normal file → Executable file
0
claude.sh
Normal file → Executable file
|
|
@ -1,20 +1,19 @@
|
||||||
FROM node:20-slim
|
FROM node:20-alpine
|
||||||
|
|
||||||
# Install minimal runtime dependencies
|
# Install runtime dependencies
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apk add --no-cache \
|
||||||
git \
|
git \
|
||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
bash \
|
bash \
|
||||||
ttyd \
|
ttyd
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Entrypoint used by the webui service (ttyd wrapping claude)
|
# Entrypoint used by the webui service (ttyd wrapping claude)
|
||||||
COPY --chmod=755 webui-entrypoint.sh /usr/local/bin/webui-entrypoint.sh
|
COPY --chmod=755 webui-entrypoint.sh /usr/local/bin/webui-entrypoint.sh
|
||||||
|
|
||||||
# Create non-root user
|
# Create non-root user
|
||||||
RUN groupadd -g 1000 claude \
|
RUN addgroup -g 1000 claude \
|
||||||
&& useradd -u 1000 -g claude -m -s /bin/bash claude
|
&& adduser -u 1000 -G claude -s /bin/bash -D claude
|
||||||
|
|
||||||
# Install Claude Code globally (runs as root for npm -g, then drops)
|
# Install Claude Code globally (runs as root for npm -g, then drops)
|
||||||
RUN npm install -g @anthropic-ai/claude-code
|
RUN npm install -g @anthropic-ai/claude-code
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
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:
|
build:
|
||||||
context: .
|
context: proxy
|
||||||
dockerfile: Dockerfile.proxy
|
dockerfile: Dockerfile
|
||||||
networks:
|
networks:
|
||||||
- claude-internal # reachable by claude and webui containers
|
- claude-internal # reachable by claude and webui containers
|
||||||
- proxy-external # has outbound internet access
|
- proxy-external # has outbound internet access
|
||||||
|
|
@ -18,7 +17,7 @@ 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 CLI container ─────────────────────────────────────────────
|
# ─── Claude Code CLI container ─────────────────────────────────────────────
|
||||||
|
|
@ -26,8 +25,8 @@ services:
|
||||||
# Run via "docker compose run --rm claude" (managed by claude.sh).
|
# Run via "docker compose run --rm claude" (managed by claude.sh).
|
||||||
claude:
|
claude:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: claude/
|
||||||
dockerfile: Dockerfile.claude
|
dockerfile: Dockerfile
|
||||||
depends_on:
|
depends_on:
|
||||||
proxy:
|
proxy:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
@ -54,8 +53,8 @@ services:
|
||||||
# Network isolation is identical to the CLI container.
|
# Network isolation is identical to the CLI container.
|
||||||
webui:
|
webui:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: claude/
|
||||||
dockerfile: Dockerfile.claude
|
dockerfile: Dockerfile
|
||||||
entrypoint: ["/usr/local/bin/webui-entrypoint.sh"]
|
entrypoint: ["/usr/local/bin/webui-entrypoint.sh"]
|
||||||
depends_on:
|
depends_on:
|
||||||
proxy:
|
proxy:
|
||||||
|
|
|
||||||
19
proxy/Dockerfile
Normal file
19
proxy/Dockerfile
Normal 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"]
|
||||||
|
|
@ -14,7 +14,7 @@ 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
|
||||||
|
|
@ -26,6 +26,8 @@ acl CONNECT method CONNECT
|
||||||
# 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 localhost
|
||||||
|
acl allowed_sites dstdomain .local
|
||||||
|
|
||||||
# ─── Access rules ─────────────────────────────────────────────────────────────
|
# ─── Access rules ─────────────────────────────────────────────────────────────
|
||||||
# Block requests to non-standard ports
|
# Block requests to non-standard ports
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue