Part of #2
D8 — Compose example + docs surface
Labels: wayfinder:grilling
Labels: wayfinder:grilling status:closed
Blocked by: D1 (closed), D2 (closed), R2 (closed), D3 (closed), D4 (closed), D5 (closed), D9 (closed)
Resolution
Docs layout — both surfaces:
docs/getting-started.md — short new section "Run chartr as a Docker container". ~15 lines: the compose one-liner path, a pointer to docs/docker.md for depth.
docs/docker.md — new page, deep dive. Table of contents:
- What you get (single-user self-hosted, ports, baked agents from D9)
- Quick start (compose +
docker run alternative)
- Configuration reference (env vars, volumes, PUID/PGID)
- Agent authentication (two patterns)
- Adding your own agents (
/opt/agents bind-mount from D5)
- Remote access (Caddy + Traefik reverse-proxy examples, requires
CHARTR_PUBLIC_URL from R2)
- Troubleshooting
- Verifying the image (cosign snippet from D7)
README.md — one-line addition to the install section pointing at docs/docker.md.
Canonical compose file — ship a real packaging/docker/compose.yaml in-repo AND embed it in docs/docker.md. Sync check: a small make or CI step diffs the doc-embedded block against the file (acceptable modest drift risk if the sync step is skipped for v1).
packaging/docker/compose.yaml (spec — not committed by this map):
services:
chartr:
image: ghcr.io/rengwu/chartr:latest # or :edge for main
container_name: chartr
ports:
- "127.0.0.1:8787:8787" # localhost-only per destination Notes
environment:
PUID: "${UID:-1000}" # D4: LSIO uid model
PGID: "${GID:-1000}"
CHARTR_PUBLIC_URL: "http://localhost:8787" # R2: required if you remap the port or reverse-proxy
# ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY: use `env_file` below
env_file:
- .env # holds API keys per D5 auth pattern (b)
volumes:
- chartr-data:/data # D3: runtime
- chartr-config:/config # D3: operator config
# Bind-mount your source repos wherever your agents want to see them:
- ${HOME}/src:/workspace:rw # D5: repo trees come from operator, not chartr's volumes
# D5 auth pattern (a): mount agent state as needed. Example for Claude Code:
# - ${HOME}/.claude:/config/agents/.claude:rw
# D5 extension point: your own agent binaries shadow the baked set.
# - ${HOME}/.local/bin:/opt/agents:ro
restart: unless-stopped
volumes:
chartr-data:
chartr-config:
Compose-averse docker run (in docs alongside the compose block):
docker run -d --name chartr \
-p 127.0.0.1:8787:8787 \
-e PUID=$(id -u) -e PGID=$(id -g) \
-e CHARTR_PUBLIC_URL=http://localhost:8787 \
-e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
-v chartr-data:/data \
-v chartr-config:/config \
-v "$HOME/src:/workspace:rw" \
--restart unless-stopped \
ghcr.io/rengwu/chartr:latest
Reverse-proxy snippets (in docs/docker.md §6). Both assume CHARTR_PUBLIC_URL=https://chartr.example.com in compose.
-
Caddy (Caddyfile):
chartr.example.com {
reverse_proxy 127.0.0.1:8787
}
Caddy handles auto-TLS, HTTP/2, and websocket upgrades automatically — no extra config needed for /ws/control and /ws/terminal/{id}.
-
Traefik (compose labels on the chartr service — alternative to the standalone reverse proxy):
labels:
- traefik.enable=true
- traefik.http.routers.chartr.rule=Host(`chartr.example.com`)
- traefik.http.routers.chartr.entrypoints=websecure
- traefik.http.routers.chartr.tls.certresolver=letsencrypt
- traefik.http.services.chartr.loadbalancer.server.port=8787
Traefik forwards websockets by default when the target is an http:// service; no Upgrade/Connection header override needed. Server-side ping frames (D2, 30s) handle any idle-timeout defaults.
Troubleshooting section entries (each is one paragraph in docs/docker.md §7):
| Symptom |
Cause |
Fix |
Blank page / connection refused at http://localhost:9000 |
CHARTR_PUBLIC_URL not set for remapped host port (R2) |
Set CHARTR_PUBLIC_URL=http://localhost:9000 and restart |
| Blank page over reverse-proxied HTTPS domain |
CHARTR_PUBLIC_URL still http://localhost:8787 (R2) |
Set to the full https://chartr.example.com |
| Files in bind-mounted repo owned by wrong uid on host |
user: set in compose (bypassed root entrypoint, D4) |
Remove user:; use PUID/PGID env instead |
| "Not on your PATH" for an agent CLI |
Agent isn't baked and no /opt/agents mount (D5, D9) |
Bind-mount your binaries dir at /opt/agents, or install into /config/… and add to PATH via compose |
| Container restarts every 30s |
HEALTHCHECK failing — chartr can't serve /api/health (D6) |
docker logs chartr — often a broken bind-mount preventing startup |
| Websocket disconnects after 60s through a proxy |
Proxy has aggressive idle timeout |
Server sends 30s pings (D2); if still dropping, raise proxy read_timeout to ≥60s |
| Agent asks to log in every time |
Agent state dir not bind-mounted (D5) |
Add e.g. -v $HOME/.claude:/config/agents/.claude:rw |
Verify-the-image snippet (§8, from D7):
cosign verify \
--certificate-identity-regexp "https://github.com/rengwu/chartr/.github/workflows/docker-publish.yml@.*" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/rengwu/chartr:latest
Downstream impact: none — this was the last ticket. Map is complete: the way to the destination is clear.
Not-yet-specified section on the map — clear reverse-proxy guidance (folded into this ticket) and health endpoint surface (D1 committed to reusing existing /api/health; nothing else to specify).
Question
Spec what lands in docs/ for the getting-started-with-docker path:
- Location: new
docs/docker.md? Section in existing docs/getting-started.md? Both?
- A
compose.yaml example that pulls together every prior decision: image ref (ghcr.io/…:latest), port bind to 127.0.0.1:PORT, volumes for state (D3), uid/gid handling (D4), bind mounts for repos and agent CLIs (D5), env vars for the serve command (D1).
- A minimal
docker run one-liner equivalent for the compose-averse.
- A "what next" pointer for remote access (reverse proxy — deferred to Not-yet-specified on the map).
- Troubleshooting stubs (permission-denied on bind mounts → D4; agent CLI not found → D5; port already in use → D1).
Deliverable: a docs outline + the exact compose.yaml the spec commits to, ready to be dropped in by the implementation effort.
Part of #2
D8 — Compose example + docs surface
Labels:
wayfinder:grillingLabels:
wayfinder:grillingstatus:closedBlocked by: D1 (closed), D2 (closed), R2 (closed), D3 (closed), D4 (closed), D5 (closed), D9 (closed)
Resolution
Docs layout — both surfaces:
docs/getting-started.md— short new section "Run chartr as a Docker container". ~15 lines: the compose one-liner path, a pointer todocs/docker.mdfor depth.docs/docker.md— new page, deep dive. Table of contents:docker runalternative)/opt/agentsbind-mount from D5)CHARTR_PUBLIC_URLfrom R2)README.md— one-line addition to the install section pointing atdocs/docker.md.Canonical compose file — ship a real
packaging/docker/compose.yamlin-repo AND embed it indocs/docker.md. Sync check: a smallmakeor CI step diffs the doc-embedded block against the file (acceptable modest drift risk if the sync step is skipped for v1).packaging/docker/compose.yaml(spec — not committed by this map):Compose-averse
docker run(in docs alongside the compose block):Reverse-proxy snippets (in
docs/docker.md§6). Both assumeCHARTR_PUBLIC_URL=https://chartr.example.comin compose.Caddy (
Caddyfile):Caddy handles auto-TLS, HTTP/2, and websocket upgrades automatically — no extra config needed for
/ws/controland/ws/terminal/{id}.Traefik (compose labels on the chartr service — alternative to the standalone reverse proxy):
Traefik forwards websockets by default when the target is an
http://service; noUpgrade/Connectionheader override needed. Server-side ping frames (D2, 30s) handle any idle-timeout defaults.Troubleshooting section entries (each is one paragraph in
docs/docker.md§7):http://localhost:9000CHARTR_PUBLIC_URLnot set for remapped host port (R2)CHARTR_PUBLIC_URL=http://localhost:9000and restartCHARTR_PUBLIC_URLstillhttp://localhost:8787(R2)https://chartr.example.comuser:set in compose (bypassed root entrypoint, D4)user:; usePUID/PGIDenv instead/opt/agentsmount (D5, D9)/opt/agents, or install into/config/…and add to PATH via composeHEALTHCHECKfailing — chartr can't serve/api/health(D6)docker logs chartr— often a broken bind-mount preventing startupread_timeoutto ≥60s-v $HOME/.claude:/config/agents/.claude:rwVerify-the-image snippet (§8, from D7):
cosign verify \ --certificate-identity-regexp "https://github.com/rengwu/chartr/.github/workflows/docker-publish.yml@.*" \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ ghcr.io/rengwu/chartr:latestDownstream impact: none — this was the last ticket. Map is complete: the way to the destination is clear.
Not-yet-specified section on the map — clear
reverse-proxy guidance(folded into this ticket) andhealth endpoint surface(D1 committed to reusing existing/api/health; nothing else to specify).Question
Spec what lands in
docs/for the getting-started-with-docker path:docs/docker.md? Section in existingdocs/getting-started.md? Both?compose.yamlexample that pulls together every prior decision: image ref (ghcr.io/…:latest), port bind to127.0.0.1:PORT, volumes for state (D3), uid/gid handling (D4), bind mounts for repos and agent CLIs (D5), env vars for the serve command (D1).docker runone-liner equivalent for the compose-averse.Deliverable: a docs outline + the exact compose.yaml the spec commits to, ready to be dropped in by the implementation effort.