Skip to content

D6: Dockerfile shape #11

Description

@Teagan42

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions