diff --git a/.dockerignore b/.dockerignore index a30310ff12..d6e24e57d8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,6 +12,12 @@ dist-ssr coverage .git .claude +# The self-host bundle's only entry point is src/server.ts (scripts/build-selfhost.mjs) and `npm ci` +# only ever sees the root package*.json (copied before the rest of the tree) — the gittensory-ui +# workspace app and the test suite are never read during the image build, so keep both out of the +# build context entirely (measured: ~11MB of this repo's ~22MB tracked-file footprint). +apps +test # Runtime-only private review config and subscription CLI auth must never enter image layers. gittensory-config **/gittensory-config diff --git a/Dockerfile b/Dockerfile index 164b72daee..7f55c11199 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,8 +45,12 @@ RUN mkdir -p /home/node/.npm-global /home/node/.npm \ && ln -s /data/codex /home/node/.codex \ && chown -h node:node /home/node/.codex \ && chown -R node:node /home/node/.npm-global /home/node/.npm +# `npm install -g` populates ~/.npm/_cacache (the download cache) as a side effect, but nothing at +# runtime ever reads it -- left alone it becomes ~180MB of dead weight baked permanently into this +# layer (measured: node_modules for both CLIs together is ~465MB, the npm cache adds another ~180MB +# on top for zero runtime benefit). `npm cache clean --force` after the install trims that for free. USER node -RUN if [ "$INSTALL_AI_CLIS" = "true" ]; then npm install -g --foreground-scripts @anthropic-ai/claude-code@2.1.187 @openai/codex@0.142.0; fi +RUN if [ "$INSTALL_AI_CLIS" = "true" ]; then npm install -g --foreground-scripts @anthropic-ai/claude-code@2.1.187 @openai/codex@0.142.0 && npm cache clean --force; fi USER root # Optional: enable visual review via an external Chrome sidecar (e.g. `browserless/chrome:latest`). # Build with `--build-arg INSTALL_VISUAL_REVIEW=true` then set BROWSER_WS_ENDPOINT= at runtime. diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx index ac3e6a9888..f06cbe10b1 100644 --- a/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx +++ b/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx @@ -131,6 +131,205 @@ docker compose --profile postgres --profile observability --profile backup up -d watching for.

+

Resource profiles

+

+ Measured rows below come from a real production instance running the full + profile set (qdrant + redis + observability +{" "} + backup + postgres + ollama) at steady state — + docker stats and docker system df snapshots, not a lab benchmark. + Estimated rows are reasoned from that same baseline plus each + service's declared deploy.resources.limits and image size in{" "} + docker-compose.yml — they have not been measured directly and could be off, + especially for CPU under real load. Treat estimates as a starting point for capacity + planning, not a guarantee. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ProfileCPU (steady state)Memory (steady state)Basis
+ Minimal — app + redis only (no profile flags) + ~3% of one core~400–600MiB + Estimated: app + redis measured in isolation from the full-profile snapshot (app + 2.6% CPU / 365MiB; redis is idle-light and its 512MiB limit is never approached in + the full-profile run either). +
+ + --profile postgres + + +14% of one core (highest single-service CPU consumer) + +~200MiB + Measured: 14.24% CPU / 196MiB of its 2GiB limit — comfortable headroom on memory, + but the largest CPU line item in the whole stack. +
+ + --profile qdrant + Low single-digit % + Well under its 2GiB limit + + Measured (part of the full-profile snapshot's "everything else" low-CPU, under-limit + group). Grows with RAG corpus size — expect this to climb on installs with many + indexed repos. +
+ + --profile observability + + Low single-digit % per service, except Grafana/Tempo below + + Grafana ~305MiB (60% of 512MiB); Tempo ~209MiB (20% of 1GiB); Prometheus/Loki/ + Alertmanager/Promtail/otel-collector each well under their limits + + Measured. Grafana is the closest any service comes to its ceiling in production — + worth watching if you add many custom dashboards or panels, but not currently a + problem (40% headroom remains). +
+ + --profile ollama + + Near-zero idle; spikes hard during inference + + Model-dependent, up to its 8GiB limit + + Estimated. Not part of the live production profile mix (that instance uses{" "} + AI_PROVIDER=codex, not Ollama) — the 8GiB default limit is sized for a + single loaded 7–8B quantized model per the compose comment, not measured against a + running model. Idle Ollama with no model pulled is cheap; a loaded model can + legitimately approach the limit, which is why it has the largest default ceiling in + the file. +
+ + --profile backup + + Near-zero except during runs + + Low, bursts during dump/restore + + Measured as part of the full-profile snapshot (no dedicated resource limit is set + for backup/backup-exporter — both are short-lived or + idle-polling processes, not sustained consumers). +
+ + --profile runners + + Unbounded by default — can starve the app under CI load + Unbounded by default + Estimated, and explicitly a known risk, not a guess about typical usage: the{" "} + runner service ships with no CPU/memory limit at all. Production + experience already documented in docker-compose.override.yml.example{" "} + found 3 uncapped runner containers starving the app for CPU on an 8-vCPU box under + real CI load — see that file for the cpu_shares/cpus{" "} + mitigation before co-locating runners with the review stack. +
+ Full profile set (qdrant + redis +{" "} + observability + backup + postgres +{" "} + ollama, no active inference, no runners) + + Postgres (~14%) dominates; everything else low single-digit % + + No service near its limit except Grafana (~60%) + + Measured, in full, on a real production instance. +
+
+ +

Disk

+

+ Measured on the same production instance: 48GB of 151GB used on the host root volume (32%) + at steady state. docker system df breakdown: +

+ +

+ The reclaimable image and build-cache space here is{" "} + expected steady state, not a leak — this instance runs{" "} + scripts/deploy-selfhost-prebuilt.sh, which rebuilds the image from the current + git checkout on every deploy and intentionally keeps prior layers around in the build cache + for faster rebuilds. The gittensory-docker-safe-prune systemd timer (below) + already runs daily against this exact instance and reclaims it on a schedule, so this is not + a number to chase down manually. +

+ +

When a compose default might need to change

+

+ Every deploy.resources.limits.memory in docker-compose.yml is + operator-overridable via .env (see the *_MEM_LIMIT variables in{" "} + .env.example). Against the measured full-profile data above, none of the + current defaults look miscalibrated enough to change: nothing sits consistently near its + limit in a way that risks an OOM kill under normal load (Grafana's ~60% is the closest + and still has real headroom), and nothing is so oversized relative to plausible usage that + it should be lowered — including Ollama's comparatively large 8GiB ceiling, which is + sized for holding one quantized model in memory, not idle overhead. The one real gap is{" "} + --profile runners, which ships with no limit at all; that is a known, + documented tradeoff (see the table above and{" "} + docker-compose.override.yml.example) rather than an oversight, since the right + ceiling depends entirely on the host's core count and how many runner replicas you run. +

+

Docker resource hygiene

Every service in docker-compose.yml caps its own container logs (10MB × 3