From 9b8562b746c6ba17d80a96c679eb4a0bea5c6739 Mon Sep 17 00:00:00 2001 From: docker-claude Date: Tue, 14 Apr 2026 22:25:38 +0200 Subject: [PATCH] feat(webui): add browser terminal interface via ttyd Adds a webui service to docker-compose that wraps Claude Code in ttyd, serving a browser-accessible terminal on port 7681. The webui reuses Dockerfile.claude (ttyd added to apt deps) with a dedicated entrypoint script that enforces WEBUI_PASSWORD before starting. Network isolation is identical to the CLI container: claude-internal only, all egress via the proxy allowlist. claude.sh gains web and web-stop commands. --- .env.example | 4 ++ CLAUDE.md | 31 +++++----- Dockerfile.claude | 4 ++ README.md | 134 +++++++++++++++++++++++++------------------- claude.sh | 71 ++++++++++++++++------- docker-compose.yml | 43 +++++++++++++- webui-entrypoint.sh | 14 +++++ 7 files changed, 209 insertions(+), 92 deletions(-) create mode 100644 webui-entrypoint.sh diff --git a/.env.example b/.env.example index d8a6d6f..00a8187 100644 --- a/.env.example +++ b/.env.example @@ -7,3 +7,7 @@ ANTHROPIC_API_KEY=sk-ant-... # Optional: mount a host directory as /workspace inside the Claude container. # If unset, a named Docker volume is used (fully isolated from the host). # WORKSPACE_DIR=/absolute/path/to/your/project + +# Web interface credentials (required for ./claude.sh web) +# WEBUI_USER=claude +# WEBUI_PASSWORD=changeme diff --git a/CLAUDE.md b/CLAUDE.md index 9af5a32..6c93531 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,35 +8,40 @@ This file provides context and guidance for working with this project. ## Architecture -Two 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 +- **`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 -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` 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. ## File Structure ``` docker-claude/ -├── claude.sh # Control script: start / stop / run / update / logs / status / shell -├── docker-compose.yml # Service definitions and network topology -├── Dockerfile.claude # Claude Code container (node:20-slim, UID 1000) -├── Dockerfile.proxy # Squid proxy sidecar (ubuntu:22.04, UID 13) +├── 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 ├── proxy/ -│ └── squid.conf # Squid ACL config — egress allowlist lives here -├── .env.example # Template for ANTHROPIC_API_KEY -├── .gitignore # Excludes .env and logs -├── .dockerignore # Keeps .env out of build context -└── README.md # User documentation +│ └── squid.conf # Squid ACL config — egress allowlist lives here +├── .env.example # Template for ANTHROPIC_API_KEY, WEBUI_PASSWORD, etc. +├── .gitignore # Excludes .env and logs +├── .dockerignore # Keeps .env out of build context +└── README.md # User documentation ``` ## Development Workflow ```bash chmod +x claude.sh -cp .env.example .env # set ANTHROPIC_API_KEY -./claude.sh start # build + start proxy + launch Claude interactively +cp .env.example .env # set ANTHROPIC_API_KEY (and WEBUI_PASSWORD for web mode) +./claude.sh start # build + start proxy + launch Claude interactively (CLI) +./claude.sh web # build + start proxy + webui (browser terminal on :7681) ./claude.sh update # rebuild images (no cache) after upstream updates ``` diff --git a/Dockerfile.claude b/Dockerfile.claude index ca847d5..9757f8a 100644 --- a/Dockerfile.claude +++ b/Dockerfile.claude @@ -6,8 +6,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ ca-certificates \ bash \ + ttyd \ && rm -rf /var/lib/apt/lists/* +# 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 diff --git a/README.md b/README.md index dde60b1..1fd96a6 100644 --- a/README.md +++ b/README.md @@ -5,34 +5,36 @@ Runs [Claude Code](https://claude.ai/code) inside an isolated Docker environment ## Architecture ``` -┌─────────────────────────────────────────────────────┐ -│ Host machine │ -│ │ -│ claude.sh (control script) │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────┐ │ -│ │ Docker: claude-secure │ │ -│ │ │ │ -│ │ ┌─────────────┐ claude-internal │ │ -│ │ │ claude │◄─────(internal only)───► │ │ -│ │ │ (UID 1000) │ │ │ │ -│ │ └─────────────┘ ┌──────┴──────┐ │ │ -│ │ │ proxy │ │ │ -│ │ │ (UID 13) │ │ │ -│ │ └──────┬──────┘ │ │ -│ │ proxy-external │ │ -│ └─────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ internet (allowlisted) │ -└─────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────┐ +│ Host machine │ +│ │ +│ claude.sh (control script) │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ Docker: claude-secure │ │ +│ │ │ │ +│ │ ┌─────────────┐ │ │ +│ │ │ claude │──┐ claude-internal │ │ +│ │ │ (UID 1000) │ │ (internal: true) │ │ +│ │ └─────────────┘ ├──────────────► ┌──────────┐ │ │ +│ │ ┌─────────────┐ │ │ proxy │ │ │ +│ │ │ webui │──┘ │ (UID 13) │ │ │ +│ │ │ (UID 1000) │ └────┬─────┘ │ │ +│ │ │ port 7681 │ proxy-external │ │ +│ │ └─────────────┘ │ │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ internet (allowlisted) │ +└──────────────────────────────────────────────────────────┘ ``` -- **`claude` container** — Claude Code, runs as UID 1000, on `claude-internal` only (no internet route) -- **`proxy` container** — Squid forward proxy, runs as UID 13, bridges `claude-internal` ↔ internet, enforces egress allowlist -- **`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; the proxy sidecar uses this for controlled outbound access +- **`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-internal`** — `internal: true`; no default gateway, containers cannot reach the internet directly +- **`proxy-external`** — Standard bridge; proxy sidecar only ## Prerequisites @@ -45,9 +47,9 @@ Runs [Claude Code](https://claude.ai/code) inside an isolated Docker environment # 1. Clone / copy this repo git clone docker-claude && cd docker-claude -# 2. Configure your API key +# 2. Configure credentials cp .env.example .env -$EDITOR .env # set ANTHROPIC_API_KEY +$EDITOR .env # set ANTHROPIC_API_KEY (and WEBUI_PASSWORD if using web mode) # 3. Make the control script executable chmod +x claude.sh @@ -55,62 +57,77 @@ chmod +x claude.sh ## Usage +### CLI mode + ```bash # Build images, start proxy, launch Claude Code interactively ./claude.sh start -# Same as start but skips image rebuild (faster on subsequent runs) +# Start proxy if needed, launch Claude Code (faster on subsequent runs) ./claude.sh run -# Stop and remove all containers (proxy + any running sessions) -./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 -``` - -### Working with host files - -By default, Claude's workspace is a named Docker volume (`claude-secure-workspace`) — fully isolated from the host. - -To mount a specific host directory: - -```bash +# Mount a host directory as the workspace WORKSPACE_DIR=$HOME/myproject ./claude.sh run ``` -The directory is mounted at `/workspace` inside the container. +### Web interface + +Serves Claude Code as a browser terminal via [ttyd](https://github.com/tsl0922/ttyd), protected by HTTP basic auth. + +```bash +# Add to .env first: +# WEBUI_PASSWORD=your-strong-password +# WEBUI_USER=claude # optional, defaults to "claude" + +./claude.sh web +# → Web interface running at http://0.0.0.0:7681 + +# To reach it from outside the sandbox host: +sbx ports --publish 7681:7681/tcp + +# Stop web interface (keeps proxy running) +./claude.sh web-stop +``` + +### Other commands + +```bash +./claude.sh stop # Stop and remove all containers +./claude.sh update # Rebuild images without cache +./claude.sh logs # Tail proxy logs +./claude.sh logs webui # Tail web interface logs +./claude.sh status # Show container status +./claude.sh shell # Debug bash shell in the Claude container +``` + +### Workspace + +| Mode | Default | Override | +|---|---|---| +| CLI (`run`/`start`) | Named Docker volume (isolated) | `WORKSPACE_DIR=/path ./claude.sh run` | +| Web (`web`) | Named Docker volume (`claude-web-workspace`) | Edit `docker-compose.yml` volumes | ## Egress allowlist Edit `proxy/squid.conf` and add domains to the `allowed_sites` ACL: -```squid +``` acl allowed_sites dstdomain api.anthropic.com acl allowed_sites dstdomain statsig.anthropic.com -# acl allowed_sites dstdomain api.github.com # uncomment if needed +# acl allowed_sites dstdomain api.github.com # acl allowed_sites dstdomain registry.npmjs.org ``` -Rebuild the proxy after changes: +Rebuild after changes: ```bash -docker compose -p claude-secure build proxy +./claude.sh update ./claude.sh stop && ./claude.sh start ``` ## Security controls -| Control | Claude container | Proxy container | +| Control | claude / webui | proxy | |---|---|---| | Non-root user | UID 1000 (`claude`) | UID 13 (`proxy`) | | `no-new-privileges` | yes | yes | @@ -118,3 +135,4 @@ docker compose -p claude-secure build proxy | Direct internet access | no (`internal` network only) | allowlisted only | | Host filesystem | no mounts by default | none | | Docker socket | not mounted | not mounted | +| Web auth | basic auth (ttyd `--credential`) | n/a | diff --git a/claude.sh b/claude.sh index 52bcc3a..a302548 100755 --- a/claude.sh +++ b/claude.sh @@ -118,43 +118,76 @@ cmd_shell() { dc run --rm --entrypoint /bin/bash $(workspace_flag) claude } +cmd_web() { + check_deps + load_env + if [[ -z "${WEBUI_PASSWORD:-}" ]]; then + error "WEBUI_PASSWORD is not set. Add it to .env before starting the web interface." + exit 1 + fi + info "Building images..." + dc build + info "Starting proxy and web interface..." + dc up -d webui + local port=7681 + info "Web interface is up → http://0.0.0.0:${port}" + info "Credentials: ${WEBUI_USER:-claude} / [WEBUI_PASSWORD]" + warn "To reach it from outside this host, publish the port:" + warn " sbx ports --publish ${port}:${port}/tcp" +} + +cmd_web_stop() { + check_deps + info "Stopping web interface..." + dc stop webui + dc rm -f webui +} + cmd_help() { cat < [args] Commands: - start [args] Build images, start proxy, launch Claude Code - run [args] Start proxy if needed, launch Claude Code - stop Stop and remove all containers - update Rebuild images without cache - logs [svc] Tail logs (default: proxy) - status Show container status - shell Open a bash shell in the Claude container (debug) - help Show this message + start [args] Build images, start proxy, launch Claude Code (CLI) + run [args] Start proxy if needed, launch Claude Code (CLI) + web Build images, start proxy + web interface (browser terminal) + web-stop Stop the web interface (keeps proxy running) + stop Stop and remove all containers + update Rebuild images without cache + logs [svc] Tail logs (default: proxy) + status Show container status + shell Open a bash shell in the Claude container (debug) + help Show this message -Environment variables: - 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 or shell): + ANTHROPIC_API_KEY Required for all modes. + WORKSPACE_DIR Optional (CLI mode). Host path to mount as /workspace. Defaults to a named Docker volume (fully isolated). + WEBUI_USER Web interface username (default: claude). + WEBUI_PASSWORD Required for web mode. Basic auth password. Examples: ./claude.sh start WORKSPACE_DIR=\$HOME/myproject ./claude.sh run + ./claude.sh web ./claude.sh logs proxy + ./claude.sh logs webui ./claude.sh shell EOF } # ─── Dispatch ───────────────────────────────────────────────────────────────── case "${1:-help}" in - start) shift; cmd_start "$@" ;; - stop) cmd_stop ;; - run) shift; cmd_run "$@" ;; - update) cmd_update ;; - logs) shift; cmd_logs "${1:-}" ;; - status) cmd_status ;; - shell) cmd_shell ;; - help|-h|--help) cmd_help ;; + start) shift; cmd_start "$@" ;; + stop) cmd_stop ;; + run) shift; cmd_run "$@" ;; + web) cmd_web ;; + web-stop) cmd_web_stop ;; + update) cmd_update ;; + logs) shift; cmd_logs "${1:-}" ;; + status) cmd_status ;; + shell) cmd_shell ;; + help|-h|--help) cmd_help ;; *) error "Unknown command: ${1}" cmd_help diff --git a/docker-compose.yml b/docker-compose.yml index baadf41..89fa2e0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ services: context: . dockerfile: Dockerfile.proxy networks: - - claude-internal # reachable by the claude container + - claude-internal # reachable by claude and webui containers - proxy-external # has outbound internet access restart: unless-stopped security_opt: @@ -21,7 +21,7 @@ services: - /var/spool/squid - /var/log/squid - # ─── Claude Code container ───────────────────────────────────────────────── + # ─── Claude Code CLI container ───────────────────────────────────────────── # No direct internet access. All egress routes through the proxy sidecar. # Run via "docker compose run --rm claude" (managed by claude.sh). claude: @@ -48,6 +48,41 @@ services: # Workspace is injected by claude.sh via --volume flag at run time. # Default: named Docker volume. Override: set WORKSPACE_DIR on the host. + # ─── Claude Code web interface ───────────────────────────────────────────── + # Serves Claude Code as a browser terminal via ttyd (port 7681). + # Protected by HTTP basic auth — set WEBUI_USER / WEBUI_PASSWORD in .env. + # Network isolation is identical to the CLI container. + webui: + build: + context: . + dockerfile: Dockerfile.claude + entrypoint: ["/usr/local/bin/webui-entrypoint.sh"] + depends_on: + proxy: + condition: service_healthy + networks: + - claude-internal # only — no route to the internet + environment: + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + - HTTP_PROXY=http://proxy:3128 + - HTTPS_PROXY=http://proxy:3128 + - ALL_PROXY=http://proxy:3128 + - NO_PROXY=localhost,127.0.0.1 + - WEBUI_USER=${WEBUI_USER:-claude} + - WEBUI_PASSWORD=${WEBUI_PASSWORD:-} + - WEBUI_PORT=7681 + ports: + - "0.0.0.0:7681:7681" + volumes: + - claude-web-workspace:/workspace + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + stdin_open: true + tty: true + restart: unless-stopped + networks: # Internal-only: Docker adds no default gateway → no direct internet route claude-internal: @@ -57,3 +92,7 @@ networks: # External: standard bridge with internet access (proxy only) proxy-external: driver: bridge + +volumes: + # Persistent workspace for the web interface + claude-web-workspace: diff --git a/webui-entrypoint.sh b/webui-entrypoint.sh new file mode 100644 index 0000000..4cf31cd --- /dev/null +++ b/webui-entrypoint.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Entrypoint for the webui service. +# Wraps Claude Code in ttyd (terminal-over-WebSocket) with basic auth. +set -euo pipefail + +: "${WEBUI_PASSWORD:?WEBUI_PASSWORD must be set in .env}" +WEBUI_USER="${WEBUI_USER:-claude}" +WEBUI_PORT="${WEBUI_PORT:-7681}" + +exec ttyd \ + --port "${WEBUI_PORT}" \ + --writable \ + --credential "${WEBUI_USER}:${WEBUI_PASSWORD}" \ + claude