#218: mirror procedure into an internal registry #286

Merged
fable-5 merged 1 commits from issue-218-registry-mirror into main 2026-07-31 14:49:41 +02:00
7 changed files with 156 additions and 10 deletions
Showing only changes of commit 2f7ba65eef - Show all commits

View File

@ -19,6 +19,12 @@ COLLAB_TOKEN_SECRET=change-me-to-a-long-random-string
IMAGE_PREFIX=dorfteich
# Image tag to run: a git SHA, `test`, `int`, or a release tag like v1.2.0.
TAG=latest
# Registry prefix for THIRD-PARTY images (postgres, pandoc, gotenberg,
# caddy) — for airgapped sites pulling from an internal mirror
# (issue #218, ADR 0024). Must end with a slash, e.g.
# `registry.example.gov/mirror/`. Empty = public registries. Own images
# are covered by IMAGE_PREFIX above, which may equally carry a registry.
REGISTRY_PREFIX=
# --- ports (localhost only; the host reverse proxy routes to these) ---------
# Suggested per stage on the shared host (ONE): test 8100/8101/8102,

View File

@ -193,7 +193,7 @@ services:
<<: *logging
db:
image: postgres:17.5-alpine@sha256:6567bca8d7bc8c82c5922425a0baee57be8402df92bae5eacad5f01ae9544daa
image: ${REGISTRY_PREFIX:-}postgres:17.5-alpine@sha256:6567bca8d7bc8c82c5922425a0baee57be8402df92bae5eacad5f01ae9544daa
restart: unless-stopped
environment:
POSTGRES_USER: dorfteich
@ -213,7 +213,7 @@ services:
# on the internal network only — never exposed. Pinned image; the api reaches
# it at http://pandoc:3030. `wget` ships in the (busybox-based) image.
pandoc:
image: pandoc/core:3.6@sha256:5b8a29d9b70d5d8ca766e5d1dcfc41916b23ab79276a80527be70516110f4c1e
image: ${REGISTRY_PREFIX:-}pandoc/core:3.6@sha256:5b8a29d9b70d5d8ca766e5d1dcfc41916b23ab79276a80527be70516110f4c1e
command: ['server']
restart: unless-stopped
networks: [internal]
@ -228,7 +228,7 @@ services:
# on the internal network only — never exposed. Pinned image; the api reaches
# it at http://gotenberg:3000 and posts export HTML to its Chromium route.
gotenberg:
image: gotenberg/gotenberg:8@sha256:67097317623a503ba2a6a7e9ae8db6929a1f7e1bbd88077bacf2d325fbdab923
image: ${REGISTRY_PREFIX:-}gotenberg/gotenberg:8@sha256:67097317623a503ba2a6a7e9ae8db6929a1f7e1bbd88077bacf2d325fbdab923
restart: unless-stopped
networks: [internal]
healthcheck:
@ -244,7 +244,7 @@ services:
# `localhost` default uses Caddy's internal CA — handy for smoke tests).
# Instances behind an existing host proxy simply never enable the profile.
caddy:
image: caddy:2.10-alpine@sha256:4c6e91c6ed0e2fa03efd5b44747b625fec79bc9cd06ac5235a779726618e530d
image: ${REGISTRY_PREFIX:-}caddy:2.10-alpine@sha256:4c6e91c6ed0e2fa03efd5b44747b625fec79bc9cd06ac5235a779726618e530d
profiles: [caddy]
restart: unless-stopped
ports:

17
deploy/scripts/list-images.sh Executable file
View File

@ -0,0 +1,17 @@
#!/bin/sh
# The complete image list of the deploy stack, GENERATED from the compose
# file (issue #218, ADR 0024) — a mirror procedure that works from a
# hand-maintained list will silently miss a service one day.
#
# Usage:
# TAG=v1.2.0 IMAGE_PREFIX=gitea.101010.cloud/stwaidele/dorfteich \
# deploy/scripts/list-images.sh
#
# REGISTRY_PREFIX is deliberately left empty here: the list names the
# UPSTREAM references (with digests) that a mirror copies FROM.
set -eu
cd "$(dirname "$0")/../compose"
COMPOSE_PROJECT_NAME=dorfteich-list \
POSTGRES_PASSWORD=unused COLLAB_TOKEN_SECRET=unused \
REGISTRY_PREFIX= \
docker compose -f docker-compose.yml --profile caddy config --images | sort -u

View File

@ -151,6 +151,64 @@ deliberately not digest-pinned.
4. Verify after rollout: `docker inspect --format '{{.Image}}' <container>`
must print the pinned digest (or check `RepoDigests` on the image).
## 5b. Mirroring into an internal registry (issue #218, ADR 0024)
Airgapped sites pull every image from their own registry. The procedure
is written for the customer's operations team — nothing in it needs this
project's infrastructure.
1. **Generate the image list** (never hand-maintain it — a manual list
silently misses a service one day). On a machine with this repository
and Docker:
```bash
TAG=v0.12.0 IMAGE_PREFIX=gitea.101010.cloud/stwaidele/dorfteich \
deploy/scripts/list-images.sh
```
This resolves `deploy/compose/docker-compose.yml` (including the
optional caddy profile) and prints every reference: our four images
at the release tag plus the digest-pinned third-party images.
2. **Copy each image digest-preservingly.** The copy tool is
`docker buildx imagetools create` — it moves manifests
registry-to-registry without re-encoding, so the digest in the mirror
is byte-identical to the pinned one:
```bash
docker buildx imagetools create \
--tag registry.example.gov/mirror/postgres:17.5-alpine \
postgres:17.5-alpine@sha256:6567bca8d7bc8c82c5922425a0baee57be8402df92bae5eacad5f01ae9544daa
```
(One command per listed image; our own images copy the same way from
the release registry. Fallback when `imagetools create` stalls against
a registry — observed once with the Gitea registry: plain
`docker pull` + `docker tag` + `docker push` is equally valid, because
step 3's digest comparison closes the loop either way.)
3. **Verify the digest after the copy** — the copy is only evidence once
checked:
```bash
docker buildx imagetools inspect registry.example.gov/mirror/postgres:17.5-alpine
# → Digest: must equal the pinned sha256 from the compose file
```
4. **Point the deployment at the mirror** — configuration only, no image
reference is ever edited per site:
- third-party images: `REGISTRY_PREFIX=registry.example.gov/mirror/`
in the stage `.env` (must end with `/`),
- own images: `IMAGE_PREFIX=registry.example.gov/mirror/dorfteich`.
The digest pins still apply — Docker verifies the pulled content
against the same `sha256` regardless of which registry serves it.
Executed end-to-end on 2026-07-31 against a local `registry:2` (all four
third-party images plus `dorfteich-api:v0.12.0`; every mirrored digest
identical to the pin) — protocol:
`docs/vs-nfd/95-mirror-protokoll.md`.
## 6. Verification checklist
- [ ] `https://test.dorfteich.cloud/healthz``ok`

View File

@ -70,7 +70,7 @@ Hochgezogen, weil das eine Frage im **ersten** Behördengespräch ist. „Sollte
gehen" ist dort eine schlechtere Antwort als „getestet, hier ist die Anleitung".
- [x] Alle Images auf Digest pinnen (schließt den `gotenberg:8`-Punkt ein) · 1 AT · #203
- [ ] Mirror-Verfahren in interne Registry dokumentieren · 1 AT · #218
- [x] Mirror-Verfahren in interne Registry dokumentieren · 1 AT · #218
- [ ] Build ohne Netz reproduzierbar (pnpm Offline-Store / reine
Prebuilt-Images) · 23 AT · #219
- [ ] Testlauf in netzisolierter Umgebung, Protokoll als Beleg · 2 AT · #220

View File

@ -46,11 +46,21 @@ Belegstufe: ✅ erprobt — die Stages Test/Int/Prod auf ONE sind exakt
nach dieser Prozedur aufgesetzt und laufen produktiv (`deploy/stages.md`
dokumentiert die realen Instanzen).
**Airgap-/Offline-Variante:** ⏳ offen — Mirror-Verfahren (#218),
netzloser Build (#219), Testlauf in isolierter Umgebung (#220),
Offline-Update-Pfad (#221); Meilenstein M28. Bereits vorhanden als
Grundlage: alle Dritt-Images digest-gepinnt (#203), ein authoritativer
Node-Pin (#236), SBOMs je Release (#202).
**Airgap-/Offline-Variante:** teilweise verfügbar —
- **Mirror-Verfahren (#218): ✅ verfügbar und einmal end-to-end
ausgeführt.** Schrittfolge: Image-Liste generieren
(`deploy/scripts/list-images.sh` — nie von Hand pflegen),
digest-erhaltend kopieren (`docker buildx imagetools create`), Digest
im Spiegel gegen den Compose-Pin verifizieren, Deployment per
`REGISTRY_PREFIX` (Dritt-Images) bzw. `IMAGE_PREFIX` (eigene Images)
auf den Spiegel zeigen. Vollständige Prozedur:
`deploy/stages.md` §5b; Ausführungsnachweis:
`95-mirror-protokoll.md`.
- ⏳ offen: netzloser Build (#219), Testlauf in isolierter Umgebung
(#220), Offline-Update-Pfad (#221); Meilenstein M28. Bereits vorhanden
als Grundlage: alle Dritt-Images digest-gepinnt (#203), ein
authoritativer Node-Pin (#236), SBOMs je Release (#202).
## 2 Update und Rollback

View File

@ -0,0 +1,55 @@
# Mirror-Protokoll — Spiegelung in eine interne Registry (Issue #218)
Nachweis der einmaligen End-to-End-Ausführung des Mirror-Verfahrens aus
`deploy/stages.md` §5b (ADR 0024). Prüfer-taugliche Belegstufe: **live
verifiziert** — jede Zeile unten ist die tatsächliche Ausgabe des
Verifikationsschritts, nicht eine Erwartung.
- **Datum:** 2026-07-31
- **Umgebung:** Dritt-Images: macOS-Arbeitsplatz (OrbStack-Docker) mit
Ziel-Registry `registry:2` auf `localhost:5001`; eigenes Image: Host
ONE mit Ziel-Registry `registry:2` auf `127.0.0.1:5002`
stellvertretend für die interne Registry der Behörde; das Verfahren
ist registry-agnostisch.
- **Quelle der Image-Liste:** `deploy/scripts/list-images.sh` mit
`TAG=v0.12.0` (der aktuelle Prod-Stand), generiert aus
`deploy/compose/docker-compose.yml` inkl. caddy-Profil.
- **Kopierwerkzeug:** Dritt-Images per `docker buildx imagetools
create` (kopiert Manifeste registry-zu-registry ohne Re-Encoding —
digest-erhaltend). Eigenes Image per `docker pull` + `tag` + `push`;
der Digest-Vergleich schließt die Schleife (Beobachtung 31.07.:
`imagetools create` stallte vom Arbeitsplatz gegen die Gitea-Registry,
während die Registry-API sofort antwortete — der pull/push-Weg ist der
robuste Ausweich, solange der Digest verifiziert wird).
## Ergebnis je Image
Verifikation jeweils per `docker buildx imagetools inspect
localhost:5001/mirror/<name>` — der Digest im Spiegel muss dem Pin aus
der Compose-Datei gleichen.
| Image | Gepinnter Digest (Compose) | Digest im Spiegel | Ergebnis |
| ----------------------- | ------------------------------------------------------------------------- | ----------------- | -------- |
| `caddy:2.10-alpine` | `sha256:4c6e91c6ed0e2fa03efd5b44747b625fec79bc9cd06ac5235a779726618e530d` | identisch | OK |
| `postgres:17.5-alpine` | `sha256:6567bca8d7bc8c82c5922425a0baee57be8402df92bae5eacad5f01ae9544daa` | identisch | OK |
| `pandoc/core:3.6` | `sha256:5b8a29d9b70d5d8ca766e5d1dcfc41916b23ab79276a80527be70516110f4c1e` | identisch | OK |
| `gotenberg/gotenberg:8` | `sha256:67097317623a503ba2a6a7e9ae8db6929a1f7e1bbd88077bacf2d325fbdab923` | identisch | OK |
| `dorfteich-api:v0.12.0` | `sha256:576f16467563871a9655059fc9cdcce49e3951b54a8697065081a9a6832d2294` | identisch | OK |
(Die drei übrigen eigenen Images `web`/`collab`/`backup` folgen exakt
demselben Kommando mit anderem Namen; das Verfahren ist je Image
identisch und die Liste kommt generiert aus dem Skript — stellvertretend
wurde `api` als größtes eigenes Image kopiert.)
## Abweichungen
Alle kopierten Digests stimmen mit den Pins bzw. dem Quell-Digest
überein. Einzige Beobachtung: der Werkzeug-Stall von `imagetools
create` gegen die Gitea-Registry (oben) — dokumentiert samt Ausweichweg
in `deploy/stages.md` §5b.
## Pflege
Bei jedem Digest-Update (`deploy/stages.md` §5a) und jedem Release
wiederholt der Betreiber die Schritte 13 aus §5b für die geänderten
Images; die Liste kommt IMMER frisch aus `list-images.sh`.