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.
This commit is contained in:
docker-claude 2026-04-14 22:25:38 +02:00
parent c01102b641
commit 9b8562b746
7 changed files with 209 additions and 92 deletions

134
README.md
View file

@ -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 <repo> 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 <sandbox-name> --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 |