-
-
Notifications
You must be signed in to change notification settings - Fork 0
0.2 docker
This page documents every Dockerfile and compose file in the repository — what each stage does, how the pieces fit, and the invariants that keep the stack deterministic.
| File | Role |
|---|---|
Dockerfile (root) |
Canonical image definition. One file, many targets — dev and production for both apps. |
.dockerignore (root) |
Keeps the build context small: excludes **/node_modules, **/dist, **/.pnpm-store, real .env*. |
compose.yml (root) |
Development stack: deps (install), server, dashboard. |
infra.compose.yml |
Backing services: PostgreSQL 16, MinIO, KeyDB, Playwright MCP sidecar (+ commented Ollama, SearXNG). |
server/Dockerfile, dashboard/Dockerfile
|
Standalone per-app images — kept for a potential split of the monorepo into separate repos. Not used by compose.yml. |
Compose project state is simple: app services share one external Docker network triplef.io (created by infra.compose.yml).
The root Dockerfile builds everything from the repository root context so that the pnpm workspace — and above all the single root pnpm-lock.yaml — is the dependency authority for every stage.
FROM node:24 AS base
base carries only shared facts: corepack-prepared pnpm, and the environment the whole image relies on:
| Variable | Why it exists |
|---|---|
CI=true |
Non-interactive installs; with --frozen-lockfile this hard-fails on lockfile drift — by design. |
PNPM_CONFIG_CONFIRM_MODULES_PURGE=false |
Lets pnpm replace changed node_modules without a TTY prompt. |
PNPM_CONFIG_MINIMUM_RELEASE_AGE=0 |
Disables the workspace's release-age gate inside images. |
HUSKY=0 |
Root prepare script runs husky — disabled in containers. |
pnpm_config_store_dir=/repo/.pnpm-store |
pnpm 11 only honours pnpm_config_* env vars (the legacy PNPM_STORE_DIR is ignored). Store lives inside the mounted workspace → shared between host and containers, hardlink-friendly, persistent. |
PATH="$PNPM_HOME:$PATH" |
Corepack shims on PATH. Split into its own ENV line — same-line self-references warn under BuildKit. |
No pnpm version is baked into the image on purpose: package.json's packageManager: pnpm@11.17.0 field drives corepack — update it once, every image follows.
| Target | Parent | Purpose |
|---|---|---|
local |
base |
Image for the compose dev stack. Contains no sources — the repo is bind-mounted at runtime. pnpm_config_verify_deps_before_run=false (see below). |
deps |
base |
Copies pnpm-lock.yaml, pnpm-workspace.yaml, root + app package.jsons and server/prisma, then pnpm install --frozen-lockfile with a BuildKit cache mount on the store. Reused as parent of all build stages. |
buildserver-dev / buildserver-prod
|
deps |
Copy the minimal server build context (tsconfig*.json, shims.d.ts, src/), run pnpm --filter "{server}" run build[:prod], then pnpm deploy --legacy into /prod/server. |
builddashboard / builddashboard-dev
|
deps |
Same idea for the dashboard (build = vue-tsc -b && vite build). |
server-development / server-production
|
base |
Runtime stages receiving the deployed app + compiled dist. Production entrypoint: pnpm run start:node (node dist/main). |
dashboard-development / dashboard-production
|
base |
Analogous; production serves vite preview on 4173. |
Two deliberate choices worth knowing:
-
Minimal server build context.
server/tsconfig.jsonincludes stray root-level configs (eslint.config.ts,vitest.config.ts, …). If those files are present in the build context, tsc recomputes the common root abovesrc/and nests output intodist/src/— breakingnode dist/main. Copying only what the build needs keepsdist/main.jsdeterministic. -
pnpm deploy --legacy. pnpm v10+ refusesdeployunlessinjectWorkspacePackages=true(injected mode). The apps do not depend on each other, so the legacy deploy path is correct and needs no workspace-semantics change.
docker compose build # dev image (target: local)
docker build --target server-production . # context is the REPO ROOT
docker build --target dashboard-development .Production images are self-contained (deps baked), sized ≈2 GB thanks to the flattened root context + .dockerignore (previously 3.2–3.6 GB with accidental node_modules copies).
deps ──(completed)──▶ server ──(started)──▶ dashboard
| Service | Command | Notes |
|---|---|---|
deps |
pnpm install --frozen-lockfile --prefer-offline at /repo
|
One-shot; exits 0; the single owner of node_modules state. |
server |
pnpm start:dev in /repo/server
|
NestJS watch mode, port 3000, restart: on-failure. |
dashboard |
pnpm dev in /repo/dashboard
|
Vite, port 5173, host 0.0.0.0, path /dashboard/. |
All three mount the repository at /repo — there is no per-service copy of anything, and the install deps produces is immediately and identically visible in the app containers (and on the host).
One invariant: one workspace, one lockfile, one node_modules layout.
- Host and containers share the workspace-shared pnpm layout: member packages link into the root
.pnpmstore (server/node_modules/typescript → ../../node_modules/.pnpm/…). - pnpm continues whichever layout existing state indicates. If a member directory holds a standalone layout (own
.pnpm, ownpnpm-lock.yaml) — e.g. produced by the standalone per-app Dockerfiles or a stray in-package install — a later install happily re-enters that regime, regenerates per-package lockfiles, and the next frozen install explodes withERR_PNPM_OUTDATED_LOCKFILE. The pre-rework habit of deletingnode_modules+ lockfiles before rebuilds was exactly this oscillation being reset by hand. - Guardrails now in place:
-
depsruns installs only at the repo root, always--frozen-lockfile(fails loudly on drift instead of silently rewriting the lockfile). - pnpm 11's
verifyDepsBeforeRun=installdefault (everypnpm runperforms a deps check that may silently install) is disabled in runtime stages viapnpm_config_verify_deps_before_run=false— an app container at boot never rewrites dependency state. - Per-app
pnpm-lock.yamlfiles are gone; the store/repo/.pnpm-storeis bind-mounted, already git-ignored, and survives container recreation.
-
git pull # pnpm-lock.yaml changed?
docker compose up deps # converge node_modules (seconds, warm store)
docker compose up -d --force-recreate # restart app containersOr add something: pnpm --filter server add <pkg> on the host or via docker compose exec.
infra.compose.yml is deliberately separate so docker compose down never destroys data volumes (postgres_data, minio_data, keydb_data). KeyDB is tuned by server/keydb.conf (mounted read-only into the container).
The playwright-mcp service runs the official mcr.microsoft.com/playwright/mcp headless-chromium sidecar. It is internal-only: the server reaches it at http://playwright-mcp:8931/mcp (no host port published to the LAN), with a single loopback-only publish 127.0.0.1:8931:8931 so host-side MCP clients (pi via pi-mcp-extension) can reach it as http://localhost:8931/mcp. --allowed-hosts entries must match the Host header verbatim (port included). It needs shm_size: 2gb for Chromium. See the playwright skill for operations and debugging.
server/Dockerfile and dashboard/Dockerfile build each app with ./server / ./dashboard as context and generate their own standalone pnpm setup at build time (no workspace lockfile involved). They exist because the monorepo may be sliced back into separate repositories — keep them working, but do not mix them with the workspace flow:
- Their builds never touch your working tree (installs happen in image layers).
- Running their containers against the repo bind-mount will flip
node_modulesinto the standalone regime (they runpnpm installper-app). If that happens, reset once per 0.1-quick-start → rule 3. -
server/.dockerignoreanddashboard/.dockerignorekeep their contexts lean (previously the dashboard context shipped 1.6 GB includingnode_modulesinto the daemon).
| Symptom | Cause | Fix |
|---|---|---|
ERR_PNPM_OUTDATED_LOCKFILE at deps or image build |
Root package.json drift vs pnpm-lock.yaml
|
pnpm install at repo root (host, once), commit the lockfile |
| Apps crash-loop, modules “not found” right after pulling | Stale node_modules vs new lockfile |
docker compose up deps && docker compose up -d --force-recreate |
Per-app pnpm-lock.yaml reappears |
Something ran pnpm install inside a member dir against stale state |
Remove them, reset per rule 3, and run root installs only |
| First dashboard start is slow | Vite dependency pre-bundling on the fresh volume | Normal; subsequent starts are instant |
EACCES writing under /repo
|
Container UID vs host file ownership | Services run as 1000:1000; keep project files owned by your user |
Production image fails at node dist/main with missing module |
Server build saw stray root-level .ts configs (dist/src/ nesting) |
Already prevented via minimal build context — do not blanket-copy server/ into build stages |
-
Root lockfile is law — installs happen at the workspace root with
--frozen-lockfile. -
One owner for dependency state — the
depsservice; app containers merely consume it (verify-before-run disabled). - Bind mounts over COPY for dev — hot reload, zero rebuilds for source edits.
-
Minimal build contexts — deterministic tsc output, small images, honest
.dockerignore. -
Corepack, not baked versions —
packageManageris the single version truth.