Live-verified the container_update role's strategy_dockerfile.yml task file exists at the exact path the KB names, stamped the source note. Replaced the mermaid cheap-check flowchart with a real branch-shaped Nextcloud diagram. Added: the real Vault-backed push auth mechanism (Gitea PAT -> Vault Agent -> docker login --password-stdin, same pattern as everywhere else in the fleet), the real docker manifest inspect command instead of a vague "digest check", real app names in the holdouts table instead of generic descriptions (matching how the rest of this repo already names real apps), the anonymous-volume-pruning detail, and a real (redacted) screenshot of the actual Gitea package registry showing genuine self-built container images.
51 lines
5.7 KiB
Markdown
51 lines
5.7 KiB
Markdown
# 📦 Self-Hosted Registry & Build Pipeline
|
|
|
|
> [!NOTE]
|
|
> Gitea's own built-in container registry hosts every image this fleet builds — no separate registry service to operate, no dependency on Docker Hub or ghcr at runtime for the stacks that have opted in. This is the mechanism that makes [`docs/iac-pipeline.md`](iac-pipeline.md)'s "own everything in the stack" philosophy actually true at the container-image layer, not just the OS layer.
|
|
|
|
| | |
|
|
|---|---|
|
|
| **Registry** | Gitea's own built-in container registry |
|
|
| **Trigger** | Weekly schedule via Kestra, not push-triggered |
|
|
| **Rollout** | All 5 Docker hosts converted; the largest has 36 of 41 stacks on this pattern |
|
|
|
|
## Why self-host the registry instead of just using Docker Hub
|
|
|
|
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
|
|
|
|

|
|
|
|
> [!TIP]
|
|
> Most stacks use a minimal `FROM <upstream-image>:${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 | Why it's held out |
|
|
|---|---|
|
|
| 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
|
|
|
|
> [!WARNING]
|
|
> This is schedule-triggered, not push-triggered — a deliberate choice, not a missing feature. Editing a stack's own Dockerfile doesn't by itself trigger a rebuild; it waits for the next weekly sweep to find the upstream base image has actually changed, or for someone to force a manual run via [`docs/orchestration.md`](orchestration.md). The tradeoff: a simpler mental model and no rebuild storm on every unrelated commit, at the real cost of same-day propagation for an urgent base-image CVE fix — accepted because a weekly cadence matches this fleet's actual risk tolerance, not because push-triggering would have been harder to build.
|
|
|
|
## 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. 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/)
|