Part of #2
D6 — Dockerfile shape
Labels: wayfinder:grilling
Labels: wayfinder:grilling status:closed
Blocked by: D1 (closed), R2 (closed), D3 (closed), D4 (closed), D5 (closed), D9 (closed)
Resolution
Annotated pseudocode Dockerfile. This is not a runnable file — comments encode decisions; implementers convert.
# syntax=docker/dockerfile:1.7 # BuildKit; enables cache mounts
# ────────────────────────────────────────────────────────────────────
# Stage 1 — web: build the Svelte SPA into web/dist
# ────────────────────────────────────────────────────────────────────
FROM node:22-bookworm-slim AS web # LTS pinned via NodeSource-equivalent tag; debian family for parity with final stage
WORKDIR /src/web
COPY web/package.json web/package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \ # cache-mount npm; survives buildx GHA cache
npm ci
COPY web/ ./
RUN --mount=type=cache,target=/root/.npm \
npm run build # emits web/dist/ that web/embed.go will `//go:embed all:dist`
# ────────────────────────────────────────────────────────────────────
# Stage 2 — go: build the chartr binary (SPA embedded)
# ────────────────────────────────────────────────────────────────────
FROM golang:1.23-bookworm AS go # debian family; CGO_ENABLED default off is fine
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/root/.cache/go-build \ # cache-mount Go build cache
--mount=type=cache,target=/go/pkg/mod \ # cache-mount module cache
go mod download
COPY . .
COPY --from=web /src/web/dist ./web/dist # graft SPA in before embed
ARG VERSION=dev # supplied by GHCR workflow (D7)
ARG COMMIT=unknown
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
CGO_ENABLED=0 go build \
-trimpath \
-ldflags "-s -w -X main.version=${VERSION} -X main.commit=${COMMIT}" \
-o /out/chartr ./cmd/chartr # single static binary; webview build-tag NOT set
# ────────────────────────────────────────────────────────────────────
# Stage 3 — final: debian-slim runtime + baked agents + gosu + tini
# ────────────────────────────────────────────────────────────────────
FROM debian:bookworm-slim AS final
# System deps + NodeSource repo for a pinned node major + python + git + gosu + tini
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl gnupg \
git \
python3 python3-pip \
gosu tini \
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ # per D5: pinned LTS via NodeSource
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*
# Baked agents (per D9). Float on @latest at build time; image digest is the anchor.
# Fail loudly if any install fails — do NOT `|| true`.
RUN --mount=type=cache,target=/root/.npm \
npm install -g \
@anthropic-ai/claude-code@latest \
@openai/codex@latest \
@google/gemini-cli@latest \
opencode-ai@latest # verify exact package name at implementation (D9 flag)
RUN --mount=type=cache,target=/root/.cache/pip \
pip3 install --break-system-packages --prefix=/usr/local aider-chat
# Baked-agent manifest for `chartr agents list-baked` (D9)
RUN mkdir -p /etc/chartr \
&& printf 'claude-code\ncodex\ngemini\nopencode\naider\n' > /etc/chartr/baked-agents
# Volumes + entrypoint script + PATH extension dir
RUN mkdir -p /data /config /opt/agents \
&& groupadd -g 1000 chartr \
&& useradd -m -u 1000 -g 1000 -s /bin/bash chartr # baseline user; PUID/PGID entrypoint may adjust
COPY packaging/docker/chartr-entrypoint /usr/local/bin/chartr-entrypoint
RUN chmod +x /usr/local/bin/chartr-entrypoint
COPY --from=go /out/chartr /usr/local/bin/chartr
# ────────────── Env contract (per D1, D3, D5) ──────────────
ENV PATH="/opt/agents:/usr/local/bin:/usr/bin:/bin" \
CHARTR_ADDR="0.0.0.0:8787" \
CHARTR_DATA_DIR="/data" \
XDG_CONFIG_HOME="/config" \
CHARTR_NO_PATH_PROBE="1" \
CHARTR_IN_CONTAINER="1" \
PUID="1000" \
PGID="1000"
VOLUME ["/data", "/config"]
EXPOSE 8787
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -fsS http://127.0.0.1:8787/api/health || exit 1 # curl is already installed for NodeSource; keep it
# ────────────── OCI labels (per D9 + D7) ──────────────
LABEL org.opencontainers.image.source="https://github.com/rengwu/chartr" \
org.opencontainers.image.description="chartr — AI workspace with a map of your work" \
org.opencontainers.image.licenses="see LICENSE in repo" \
chartr.agents="claude-code,codex,gemini,opencode,aider"
# NOTE: image.version + image.revision are set by the GHCR workflow (D7) via `docker/metadata-action`, not hardcoded.
# USER stays root — required so the entrypoint can chown + gosu-drop (per D4).
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/chartr-entrypoint"] # tini first, then entrypoint script, then chartr
CMD [] # no default flags — env drives everything
Entrypoint script (packaging/docker/chartr-entrypoint, POSIX sh):
#!/bin/sh
# Per D4: adjust uid/gid, chown chartr volumes only, gosu into chartr.
# Never touches /opt/agents (read-only host mount) or /workspace (bind-mounted repos).
set -e
PUID="${PUID:-1000}"
PGID="${PGID:-1000}"
# Reconcile the `chartr` user/group with PUID/PGID (idempotent, tolerates drift).
groupmod -o -g "$PGID" chartr 2>/dev/null || true
usermod -o -u "$PUID" -g "$PGID" chartr 2>/dev/null || true
# chown ONLY chartr-owned volumes. Never /opt/agents; never any bind-mounted workspace path.
chown -R "$PUID:$PGID" /data /config
exec gosu chartr:chartr /usr/local/bin/chartr
Non-Dockerfile facts also decided here:
- Final
USER: root — required by D4's entrypoint. gosu drops to chartr.
WORKDIR: not set on the final stage (chartr is fully env-driven; there's no meaningful cwd default).
- Image size target: not enforced, but expect ~500-800MB with node+python+git+baked agents. If it lands materially above that, the "slim vs full variant" fog on the map graduates.
- Base pinned to
bookworm-slim (debian 12) explicitly, not stable-slim — Reproducibility across the transition when debian's stable bumps.
Downstream impact:
- D7 (GHCR workflow) — supplies
VERSION and COMMIT build-args and injects image.version + image.revision labels via docker/metadata-action; wires type=gha cache for the cache mounts.
- D8 (compose + docs) — mentions
HEALTHCHECK is built in; troubleshooting entry for "container restarts every 30s" pointing at a broken bind-mount that blocks chartr from serving.
- Small Go work reminder (from D9):
chartr agents list-baked reads /etc/chartr/baked-agents.
Blocks: D7
Question
Spec the Dockerfile without writing it. Decisions:
- Base image for the final stage:
debian:*-slim, alpine, gcr.io/distroless/*, or ubuntu:*? Weigh against D5's pre-installed runtime deps, D4's uid model, and CGO needs of the Go build.
- Build stage:
golang:* + node:* (for web/ build) — one stage or two? Where does web/dist land before Go embeds it?
- Multi-stage layout: named stages, cache-mount friendliness (
--mount=type=cache for Go module + npm caches).
- Entrypoint and CMD: implied by D1's serve-command shape; document tini/dumb-init use for zombie reaping of PTY children.
EXPOSE, VOLUME, USER, WORKDIR declarations — enumerate from D3, D4.
- Image labels: OCI labels (
org.opencontainers.image.source, revision, version) required for GHCR + Packages linkage.
Deliverable: an annotated pseudocode Dockerfile (comments-as-decisions), NOT a working file. Implementation effort will convert it.
Part of #2
D6 — Dockerfile shape
Labels:
wayfinder:grillingLabels:
wayfinder:grillingstatus:closedBlocked by: D1 (closed), R2 (closed), D3 (closed), D4 (closed), D5 (closed), D9 (closed)
Resolution
Annotated pseudocode Dockerfile. This is not a runnable file — comments encode decisions; implementers convert.
Entrypoint script (
packaging/docker/chartr-entrypoint, POSIX sh):Non-Dockerfile facts also decided here:
USER:root— required by D4's entrypoint. gosu drops to chartr.WORKDIR: not set on the final stage (chartr is fully env-driven; there's no meaningful cwd default).bookworm-slim(debian 12) explicitly, notstable-slim— Reproducibility across the transition when debian'sstablebumps.Downstream impact:
VERSIONandCOMMITbuild-args and injectsimage.version+image.revisionlabels viadocker/metadata-action; wirestype=ghacache for the cache mounts.HEALTHCHECKis built in; troubleshooting entry for "container restarts every 30s" pointing at a broken bind-mount that blocks chartr from serving.chartr agents list-bakedreads/etc/chartr/baked-agents.Blocks: D7
Question
Spec the Dockerfile without writing it. Decisions:
debian:*-slim,alpine,gcr.io/distroless/*, orubuntu:*? Weigh against D5's pre-installed runtime deps, D4's uid model, and CGO needs of the Go build.golang:*+node:*(forweb/build) — one stage or two? Where doesweb/distland before Go embeds it?--mount=type=cachefor Go module + npm caches).EXPOSE,VOLUME,USER,WORKDIRdeclarations — enumerate from D3, D4.org.opencontainers.image.source,revision,version) required for GHCR +Packageslinkage.Deliverable: an annotated pseudocode Dockerfile (comments-as-decisions), NOT a working file. Implementation effort will convert it.