The Docker build was slower than it needed to be and could drift away from
the lockfile. Three separate causes, fixed together.
The image installed a pinned [email protected].1 while package.json pins 12.11.2
through `packageManager`. pnpm silently self-managed the difference at
build time: it downloaded the right version into an uncached location and
never verified the sha512 hash. Install corepack instead and let it resolve
version and hash from the pin, so the image can no longer diverge and a
tampered tarball is rejected. The resolved pnpm lives in COREPACK_HOME
inside the image, so the migrations image still runs pnpm offline
(`docker run --entrypoint pnpm ... db:migrate`).
The pnpm store cache-mount pointed at /root/.local/share/pnpm/store, but
pnpm keeps its store in $PNPM_HOME/store/v11. The mount was a no-op: every
build re-downloaded all 493 packages ("reused 0, downloaded 493"). Mounting
the store where pnpm actually keeps it turns that into "reused 493,
downloaded 0".
The build context carried backups/ (232MB of rotating database dumps) and
public/vendor/ (regenerated by `pnpm assets:editor` from node_modules).
Both grew the context and invalidated the `COPY . .` layer on every daily
backup rotation, forcing a full Next.js rebuild. Excluding them takes the
context from 276MB to 23MB.
Compose now also exports the build cache to .docker-cache/cms and imports it
again, so the cache survives `scripts/docker-prune.sh`, which trims the
daemon cache to 4GB after every deploy. Verified by pruning the entire
daemon cache: the next build restored every layer from the local export and
downloaded nothing.
Also: byparr's floating `:latest` tag is now pulled on every start, the
migrations build in scripts/docker-update.sh imports the same local cache,
the README no longer claims the build needs host networking or an
iptables-disabled daemon, the pnpm rationale in renovate.json reflects that
Docker follows the pin automatically, and contract tests pin the new
invariants so the store path cannot drift back.
Enable osvVulnerabilityAlerts (Gitea has no advisory feed of its own) with the
OSV summary on the dependency dashboard, and let security fixes skip the 3-day
minimumReleaseAge in their own labelled PR. Set automergeStrategy to squash so
automerged dependency updates match the repository history.
Renovate bumped .nvmrc and the packageManager pin in isolation, so the
check-node-toolchain contract failed (engines.node, @types/node and the Docker
node image are intentionally upgraded together) and the grouped PR could never
pass check. Disable the nvm manager, the Dockerfile node image and the pnpm
packageManager field so those rollouts stay manual and coordinated.
Run the pinned renovate/renovate image on the host runner every weekday at
04:17 (plus workflow_dispatch) against the gitea platform, authenticating with
the renovate-bot RENOVATE_TOKEN Actions secret. Minor and patch updates are
grouped and automerged only after CI passes; majors wait for dashboard
approval. Document the bot, secret rotation and the minimumReleaseAge policy in
the error-centre runbook.