From 1c489f86362ad34893860d5ed5304b71e29b6efc Mon Sep 17 00:00:00 2001 From: docker-claude Date: Tue, 14 Apr 2026 22:50:59 +0200 Subject: [PATCH] refactor(claude): use built-in node user instead of custom claude user Drop the addgroup/adduser layer entirely. node:20-alpine already ships a node user at uid/gid 1000. Update chown and USER directives, and update the claude-config volume mount path to /home/node/.claude. --- CLAUDE.md | 4 ++-- README.md | 6 +++--- claude/Dockerfile | 14 +++++--------- docker-compose.yml | 4 ++-- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 41d07a2..1abc17e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,8 +10,8 @@ This file provides context and guidance for working with this project. Three containers managed by Docker Compose: -- **`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 +- **`claude`** — Claude Code CLI (`node:20-alpine`), runs as the built-in `node` user (UID 1000), isolated to an internal-only Docker network +- **`webui`** — Claude Code as a browser terminal via ttyd (`node:20-alpine`), `node` user (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. diff --git a/README.md b/README.md index 769d2f4..c729c70 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,8 @@ Runs [Claude Code](https://claude.ai/code) inside an isolated Docker environment └──────────────────────────────────────────────────────────┘ ``` -- **`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 +- **`claude`** — Claude Code CLI (`node:20-alpine`), runs as the built-in `node` user (UID 1000), on `claude-internal` only +- **`webui`** — Claude Code in a browser terminal via ttyd (`node:20-alpine`), `node` user (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 @@ -161,7 +161,7 @@ Rebuild after changes: | Control | claude / webui | proxy | |---|---|---| -| Non-root user | UID 1000 (`claude`) | `squid` user | +| Non-root user | UID 1000 (`node`, built into base image) | `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/Dockerfile b/claude/Dockerfile index be509a0..1a70a05 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -11,21 +11,17 @@ RUN apk add --no-cache \ # 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 (node:20-alpine reserves gid/uid 1000 for the node user) -RUN addgroup -g 1001 claude \ - && adduser -u 1001 -G claude -s /bin/bash -D claude - -# Install Claude Code globally (runs as root for npm -g, then drops) +# Install Claude Code globally RUN npm install -g @anthropic-ai/claude-code -# Workspace and Claude config dir — both owned by claude user. +# 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/claude/.claude \ - && chown -R claude:claude /workspace /home/claude/.claude +RUN mkdir -p /workspace /home/node/.claude \ + && chown -R node:node /workspace /home/node/.claude -USER claude +USER node WORKDIR /workspace # Proxy traffic through sidecar — override at runtime if needed diff --git a/docker-compose.yml b/docker-compose.yml index e71a964..bc7df0d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,7 +43,7 @@ services: # OAuth callback — required for browser-based login (claude login) - "0.0.0.0:54545:54545" volumes: - - claude-config:/home/claude/.claude + - claude-config:/home/node/.claude # Workspace is injected by claude.sh via --volume flag at run time. # Default: named Docker volume. Override: set WORKSPACE_DIR on the host. security_opt: @@ -82,7 +82,7 @@ services: # OAuth callback — required for browser-based login (claude login) - "0.0.0.0:54545:54545" volumes: - - claude-config:/home/claude/.claude + - claude-config:/home/node/.claude - claude-web-workspace:/workspace security_opt: - no-new-privileges:true