Technical deep-dive into how HolyClaude works.
HolyClaude is a single Docker container running multiple supervised services. The architecture is designed for reliability, persistence, and zero-configuration startup.
┌─────────────────────────────────────────────────┐
│ Docker Container │
│ │
│ entrypoint.sh (runs once) │
│ ├── UID/GID remapping │
│ ├── Pre-create required files │
│ ├── bootstrap.sh (first boot only) │
│ │ ├── Copy settings.json │
│ │ ├── Copy CLAUDE.md (memory) │
│ │ ├── Configure git │
│ │ └── Create sentinel file │
│ └── exec /init (s6-overlay) │
│ │
│ s6-overlay (PID 1) │
│ ├── cloudcli (longrun) │
│ │ └── claude-code-ui --port 3001 │
│ └── xvfb (longrun) │
│ └── Xvfb :99 -screen 0 1920x1080x24 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Claude │ │ Chromium │ │ Dev Tools │ │
│ │ Code CLI │ │ headless │ │ Node, Python │ │
│ └──────────┘ └──────────┘ └──────────────┘ │
│ │
│ Bind Mounts: │
│ ~/.claude ←→ ./data/claude (host) │
│ /workspace ←→ ./workspace (host) │
└─────────────────────────────────────────────────┘
Runs every time the container starts. Responsibilities:
-
UID/GID remapping — Adjusts the
claudeuser's UID/GID to matchPUID/PGIDenvironment variables. This prevents permission mismatches between container and host files. -
Workspace ownership fix — Repairs the top-level
/workspacebind mount if Docker auto-created it asroot:rooton first start. -
File pre-creation — Ensures
~/.claude.jsonexists as a file (not a directory). Docker creates bind-mount targets as directories if they don't exist, which breaks Claude Code. -
Bootstrap trigger — Checks for sentinel file
.holyclaude-bootstrapped. If absent, runsbootstrap.sh. -
Handoff —
exec /initreplaces the entrypoint process with s6-overlay, which becomes PID 1.
Runs once on first container start. Creates the sentinel file so it doesn't re-run. Responsibilities:
- Settings — Copies
settings.jsonfrom the image to~/.claude/settings.json - Memory — Copies the variant-appropriate memory template (
claude-memory-full.mdorclaude-memory-slim.md) to~/.claude/CLAUDE.md - Git — Configures git identity from
GIT_USER_NAME/GIT_USER_EMAILenv vars - Onboarding — Creates
~/.claude.jsonwithhasCompletedOnboarding: trueto skip the first-run wizard - Permissions — Fixes file ownership to match
PUID/PGID
s6-overlay is a process supervisor designed for Docker containers. It's used instead of supervisord or systemd because:
- Proper PID 1 behavior — Handles signal forwarding and zombie reaping
- Service supervision — Restarts crashed services automatically
- Clean shutdown — Graceful stop signals to all services
- Small footprint — Minimal overhead
s6's s6-setuidgid runs services with a clean environment. Docker-compose environment variables are not automatically available to s6 services. Each service's run script must explicitly set needed variables in the env command. This is a security feature, not a bug.
#!/bin/sh
cd /workspace
exec s6-setuidgid claude env HOME=/home/claude NODE_OPTIONS=--no-deprecation WORKSPACES_ROOT=/workspace claude-code-ui --port 3001- Runs as user
claude(not root) - Sets
WORKSPACES_ROOTdirectly (can't rely on docker-compose env vars due to s6 clean environment) NODE_OPTIONS=--no-deprecationsuppresses noisy deprecation warnings- Managed as a
longrunservice — auto-restarts on crash
#!/bin/sh
exec Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp- Provides a virtual display at
:99(1920x1080, 24-bit color) - Required for Chromium, Playwright, Lighthouse — they need a display even in headless mode
-nolisten tcpprevents remote X connections (security)
s6-overlay is purpose-built for Docker. supervisord is a full process manager designed for bare-metal servers — it's heavier, requires XML configuration, and doesn't handle PID 1 responsibilities (signal forwarding, zombie reaping) out of the box.
Bootstrap copies default settings and memory. Running it every time would overwrite user customizations. The sentinel pattern means:
- First boot: fresh defaults installed
- Subsequent boots: user's customizations preserved
- Manual re-trigger: delete sentinel file
CloudCLI plugins require git clone + npm install + npm run build. Running this at container start (in bootstrap) is unreliable because:
- Bind mounts may be on network storage with permission issues
- Network may be unavailable at boot
- Adds 30+ seconds to every first boot
Baking them into the Dockerfile ensures a clean, controlled build environment.
su uses PAM authentication, which can fail with renamed users (the base image's node user renamed to claude). runuser skips PAM entirely — it's designed for scripts that need to run commands as another user.
Every configuration option has a sensible default. Most users authenticate through the CloudCLI web UI, not environment variables. Requiring a .env file adds a setup step that most users don't need. Power users can use docker-compose.full.yaml which has all options documented inline.
Bind mounts let users see and manage their data on disk. Named volumes hide data in Docker's internal storage, making backup and inspection harder. For a development workstation where users want to access their code and config files directly, bind mounts are the right choice.
The VARIANT build arg controls which packages are installed:
ARG VARIANT=fullThe variant is stored at build time in /etc/holyclaude-variant. Bootstrap reads this file to copy the correct memory template.
| Variant | npm packages | pip packages | apt packages |
|---|---|---|---|
full |
All | All | All |
slim |
Core only | Core only | No pandoc/ffmpeg/libvips |
See What's Inside for the complete package lists.
The Dockerfile uses Docker's TARGETARCH build arg to download the correct s6-overlay binary:
RUN S6_ARCH=$(case "$TARGETARCH" in arm64) echo "aarch64";; *) echo "x86_64";; esac)Supported architectures:
amd64(x86_64) — Intel/AMD servers, most VPS providersarm64(aarch64) — Apple Silicon, AWS Graviton, Raspberry Pi 4+
Build for a specific platform:
docker buildx build --platform linux/arm64 -t holyclaude .