diff --git a/assets/registry-and-build-cheap-check.svg b/assets/registry-and-build-cheap-check.svg
new file mode 100644
index 0000000..be78e47
--- /dev/null
+++ b/assets/registry-and-build-cheap-check.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/assets/registry-and-build-packages.png b/assets/registry-and-build-packages.png
new file mode 100644
index 0000000..74bf502
Binary files /dev/null and b/assets/registry-and-build-packages.png differ
diff --git a/docs/registry-and-build.md b/docs/registry-and-build.md
index 280e6fe..69e00e1 100644
--- a/docs/registry-and-build.md
+++ b/docs/registry-and-build.md
@@ -13,33 +13,29 @@
Pulling directly from Docker Hub/ghcr at runtime was the status quo before this existed, and it was replaced for a concrete reason: every container pull became a runtime dependency on an external service staying up and staying reachable through the firewall model in [`docs/firewall-and-network.md`](firewall-and-network.md). Building images in-fleet and pushing to a registry that's already inside the trust boundary means a redeploy never depends on an external registry's availability, and the exact image contents are fully within this fleet's own control rather than whatever an upstream maintainer pushed as `:latest` today.
+Pushing to the registry uses the same Vault-backed secrets pattern as everything else in [`docs/secrets-and-pki.md`](secrets-and-pki.md), not a token pasted into a CI config somewhere: a Gitea Personal Access Token scoped to `write:package` only, rendered by Vault Agent to a `0600` file on each building host and piped straight into `docker login --password-stdin` — never typed, never sitting in a config file with broader scope than it needs.
+
+This is the real registry, not a claim about it — every one of these is a genuine self-built container, not a Docker Hub pull:
+
+
+
## The cheap-check-first build decision
-```mermaid
-flowchart TD
- Start["Weekly Kestra trigger see docs/orchestration.md"] --> Check["Cheap digest check metadata only, no pull"]
- Check --> Match{"Upstream digest == fleet registry digest?"}
- Match -->|yes| Nothing["Nothing runs no pull, no build, no push"]
- Match -->|no| Pull["Pull base images"]
- Pull --> Build["docker compose build --no-cache"]
- Build --> Compare["Compare pre/post image IDs"]
- Compare --> Push["Push what actually changed"]
- Push --> Redeploy["Redeploy via shared health-check/rollback path"]
-```
+
> [!TIP]
-> Most stacks use a minimal wrapper Dockerfile around the upstream image rather than a full clone-and-build-from-source setup — simpler, builds near-instantly, can't break from upstream build tooling changing underneath it, while still achieving the real goal: an in-fleet registry with no direct Docker Hub/ghcr dependency at runtime. A handful of multi-build-image stacks (the ones with more than one real upstream component, like the identity provider in [`docs/sso-and-identity.md`](sso-and-identity.md)) skip the cheap check entirely, since a single "upstream digest" comparison doesn't make sense when there's more than one upstream involved.
+> Most stacks use a minimal `FROM :${VERSION}` wrapper Dockerfile rather than a full clone-and-build-from-source setup — simpler, builds near-instantly, can't break from upstream build tooling changing underneath it, while still achieving the real goal: an in-fleet registry with no direct Docker Hub/ghcr dependency at runtime. A handful of multi-build-image stacks (the ones with more than one real upstream component, like Paperless-ngx, Ghost, Ollama, and the Grafana stack) skip the cheap check entirely, since a single "upstream digest" comparison doesn't make sense when there's more than one upstream involved.
## Deliberate holdouts
Not every stack is on this pattern, and that's a decision, not an oversight — knowing when *not* to apply a pattern uniformly is as much a design skill as building the pattern in the first place:
-| Stack type | Why it's held out |
+| Stack | Why it's held out |
|---|---|
-| Complex multi-service upstream builds (e.g. the identity provider, a photo-management app, a media transcoding tool) | Not worth replicating a build process someone else already maintains well — the maintenance cost of tracking upstream's own build changes would exceed the benefit |
-| A docker-socket-proxy style stack | Simple enough that a custom build adds no real value over the upstream image directly |
-| One database service still on `latest` | Deliberately not pinned to this pattern — low enough stakes that the extra build step isn't worth it |
-| A productivity suite with pre-existing custom Dockerfiles | Left as-is beyond adding image references — its own DB/cache still float on Docker Hub tags directly, a conscious "don't touch what already works" call |
+| Authentik, Immich, tdarr | Complex multi-service upstream builds — not worth replicating a build process the upstream project already maintains well; the maintenance cost of tracking their own build changes would exceed the benefit |
+| docker-socket-proxy | Simple enough that a custom build adds no real value over the upstream image directly |
+| Kodi's mariadb service | Deliberately still on `latest`, unpinned — low enough stakes that the extra build step isn't worth it |
+| Nextcloud's own `nextcloud`/`euro-office` services | Pre-existing custom Dockerfiles, left as-is beyond adding `image:` references — its own db/redis still float on Docker Hub tags directly, a conscious "don't touch what already works" call |
## Trigger discipline
@@ -48,7 +44,7 @@ Not every stack is on this pattern, and that's a decision, not an oversight —
## Housekeeping
-Unused local images get pruned automatically at the end of every build pass — for build-and-push-only stacks, the registry is the source of truth, not whatever happens to be sitting on local disk, so nothing is lost by aggressively reclaiming local space immediately.
+Unused local images get pruned automatically at the end of every build pass — for build-and-push-only stacks, the registry is the source of truth, not whatever happens to be sitting on local disk, so nothing is lost by aggressively reclaiming local space immediately. A volume prune runs right after it, for a less obvious reason: build-and-push-only database stacks declare no `volumes:` block of their own, but the base image's own `VOLUME` directive still leaves an anonymous volume behind on every single build regardless — always safe to reclaim immediately, since the prune only ever removes volumes with zero remaining container references.
---
Related: [`docs/iac-pipeline.md`](iac-pipeline.md), [`docs/orchestration.md`](orchestration.md), [`docs/sso-and-identity.md`](sso-and-identity.md) · Stacks: [`stacks/platform/gitea/`](../stacks/platform/gitea/), [`stacks/platform/kestra/`](../stacks/platform/kestra/)