Context
Compared our Docker setup against HolyClaude, a battle-tested containerized Claude Code environment. Several of their patterns solve real bugs in our current setup.
Must Fix
1. Node version mismatch
Dockerfile uses node:24, Dockerfile.sandbox uses node:22-bookworm-slim. Standardize on node:24.
2. UID/GID remapping in sandbox entrypoint
The sandbox hardcodes USER gsd (UID 1000). Anyone whose host UID isn't 1000 gets permission errors on the mounted /workspace. Add an entrypoint script that remaps UID/GID via env vars (PUID/PGID), matching HolyClaude's approach.
3. Pre-create critical files before bind-mount
Docker creates missing bind-mount targets as directories. If a user mounts a config file path that doesn't exist yet, it becomes a directory and breaks things. The entrypoint should pre-create known file targets (e.g. config JSON) before they're needed.
4. Disconnected multi-stage Dockerfile
Stage 1 (builder) and Stage 2 (runtime) in the root Dockerfile share nothing — no COPY --from=builder. They're two independent images pretending to be a multi-stage build. Either split into two Dockerfiles or make stage 2 actually consume builder artifacts.
Pure Wins
5. Sentinel-based bootstrap (run first-boot setup once)
Add a sentinel file check in the entrypoint. First boot runs setup (copy default configs, init git identity, etc). Subsequent boots skip it, preserving user customizations.
SENTINEL="$HOME/.gsd/.bootstrapped"
if [ ! -f "$SENTINEL" ]; then
/usr/local/bin/bootstrap.sh
touch "$SENTINEL"
fi
6. Two compose files (minimal + full)
Split docker-compose.yml into:
docker-compose.yaml — zero-config, just works
docker-compose.full.yaml — every option documented inline
7. Proper entrypoint with signal handling
Replace bare ENTRYPOINT ["gsd"] with an entrypoint script that handles UID remapping, bootstrap, and execs into the main process with proper signal forwarding.
Out of Scope
- s6-overlay (overkill unless we add sidecar services)
- Multi-arch CI/CD (separate effort)
- Notification hooks (nice-to-have, not a Docker fix)
Context
Compared our Docker setup against HolyClaude, a battle-tested containerized Claude Code environment. Several of their patterns solve real bugs in our current setup.
Must Fix
1. Node version mismatch
Dockerfileusesnode:24,Dockerfile.sandboxusesnode:22-bookworm-slim. Standardize on node:24.2. UID/GID remapping in sandbox entrypoint
The sandbox hardcodes
USER gsd(UID 1000). Anyone whose host UID isn't 1000 gets permission errors on the mounted/workspace. Add an entrypoint script that remaps UID/GID via env vars (PUID/PGID), matching HolyClaude's approach.3. Pre-create critical files before bind-mount
Docker creates missing bind-mount targets as directories. If a user mounts a config file path that doesn't exist yet, it becomes a directory and breaks things. The entrypoint should pre-create known file targets (e.g. config JSON) before they're needed.
4. Disconnected multi-stage Dockerfile
Stage 1 (
builder) and Stage 2 (runtime) in the rootDockerfileshare nothing — noCOPY --from=builder. They're two independent images pretending to be a multi-stage build. Either split into two Dockerfiles or make stage 2 actually consume builder artifacts.Pure Wins
5. Sentinel-based bootstrap (run first-boot setup once)
Add a sentinel file check in the entrypoint. First boot runs setup (copy default configs, init git identity, etc). Subsequent boots skip it, preserving user customizations.
6. Two compose files (minimal + full)
Split
docker-compose.ymlinto:docker-compose.yaml— zero-config, just worksdocker-compose.full.yaml— every option documented inline7. Proper entrypoint with signal handling
Replace bare
ENTRYPOINT ["gsd"]with an entrypoint script that handles UID remapping, bootstrap, andexecs into the main process with proper signal forwarding.Out of Scope