#218: mirror procedure into an internal registry
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m11s
CI / Build container images (pull_request) Successful in 1m24s
CI / Import/export fidelity gate (pull_request) Successful in 57s
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Auth e2e pack (pull_request) Successful in 8m49s
CD / Build and push images (push) Successful in 20s
CD / Deploy to Test (push) Successful in 13s
CD / Smoke tests against Test (push) Successful in 1m25s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m24s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Has been cancelled
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m11s
CI / Build container images (pull_request) Successful in 1m24s
CI / Import/export fidelity gate (pull_request) Successful in 57s
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Auth e2e pack (pull_request) Successful in 8m49s
CD / Build and push images (push) Successful in 20s
CD / Deploy to Test (push) Successful in 13s
CD / Smoke tests against Test (push) Successful in 1m25s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m24s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Has been cancelled
Airgapped sites pull from their own registry (ADR 0024). The image list
is GENERATED (deploy/scripts/list-images.sh resolves the compose file
incl. the caddy profile) so a mirror can never silently miss a service;
third-party images gain a configurable ${REGISTRY_PREFIX:-} in the
compose file (digest pins unchanged - Docker verifies the same sha256
regardless of which registry serves it), own images keep IMAGE_PREFIX;
no image reference is ever edited per site.
Step-by-step procedure in deploy/stages.md 5b: generate list, copy
digest-preservingly (docker buildx imagetools create; plain
pull/tag/push as the documented fallback - the digest comparison closes
the loop either way), verify the digest in the mirror against the pin,
point the deployment via REGISTRY_PREFIX/IMAGE_PREFIX.
Executed once end-to-end and recorded as assessor-facing evidence
(docs/vs-nfd/95-mirror-protokoll.md): all four third-party images
mirrored digest-identically into a local registry:2, plus
dorfteich-api:v0.12.0 (sha256:576f1646... identical on both sides; the
imagetools stall against the Gitea registry is recorded with its
workaround). Operations manual's airgap section now lists the mirror
part as available.
Refs #218.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8
This commit is contained in:
parent
6aac785841
commit
2f7ba65eef
@ -19,6 +19,12 @@ COLLAB_TOKEN_SECRET=change-me-to-a-long-random-string
|
|||||||
IMAGE_PREFIX=dorfteich
|
IMAGE_PREFIX=dorfteich
|
||||||
# Image tag to run: a git SHA, `test`, `int`, or a release tag like v1.2.0.
|
# Image tag to run: a git SHA, `test`, `int`, or a release tag like v1.2.0.
|
||||||
TAG=latest
|
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) ---------
|
# --- ports (localhost only; the host reverse proxy routes to these) ---------
|
||||||
# Suggested per stage on the shared host (ONE): test 8100/8101/8102,
|
# Suggested per stage on the shared host (ONE): test 8100/8101/8102,
|
||||||
|
|||||||
@ -193,7 +193,7 @@ services:
|
|||||||
<<: *logging
|
<<: *logging
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:17.5-alpine@sha256:6567bca8d7bc8c82c5922425a0baee57be8402df92bae5eacad5f01ae9544daa
|
image: ${REGISTRY_PREFIX:-}postgres:17.5-alpine@sha256:6567bca8d7bc8c82c5922425a0baee57be8402df92bae5eacad5f01ae9544daa
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: dorfteich
|
POSTGRES_USER: dorfteich
|
||||||
@ -213,7 +213,7 @@ services:
|
|||||||
# on the internal network only — never exposed. Pinned image; the api reaches
|
# on the internal network only — never exposed. Pinned image; the api reaches
|
||||||
# it at http://pandoc:3030. `wget` ships in the (busybox-based) image.
|
# it at http://pandoc:3030. `wget` ships in the (busybox-based) image.
|
||||||
pandoc:
|
pandoc:
|
||||||
image: pandoc/core:3.6@sha256:5b8a29d9b70d5d8ca766e5d1dcfc41916b23ab79276a80527be70516110f4c1e
|
image: ${REGISTRY_PREFIX:-}pandoc/core:3.6@sha256:5b8a29d9b70d5d8ca766e5d1dcfc41916b23ab79276a80527be70516110f4c1e
|
||||||
command: ['server']
|
command: ['server']
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks: [internal]
|
networks: [internal]
|
||||||
@ -228,7 +228,7 @@ services:
|
|||||||
# on the internal network only — never exposed. Pinned image; the api reaches
|
# 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.
|
# it at http://gotenberg:3000 and posts export HTML to its Chromium route.
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: gotenberg/gotenberg:8@sha256:67097317623a503ba2a6a7e9ae8db6929a1f7e1bbd88077bacf2d325fbdab923
|
image: ${REGISTRY_PREFIX:-}gotenberg/gotenberg:8@sha256:67097317623a503ba2a6a7e9ae8db6929a1f7e1bbd88077bacf2d325fbdab923
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks: [internal]
|
networks: [internal]
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@ -244,7 +244,7 @@ services:
|
|||||||
# `localhost` default uses Caddy's internal CA — handy for smoke tests).
|
# `localhost` default uses Caddy's internal CA — handy for smoke tests).
|
||||||
# Instances behind an existing host proxy simply never enable the profile.
|
# Instances behind an existing host proxy simply never enable the profile.
|
||||||
caddy:
|
caddy:
|
||||||
image: caddy:2.10-alpine@sha256:4c6e91c6ed0e2fa03efd5b44747b625fec79bc9cd06ac5235a779726618e530d
|
image: ${REGISTRY_PREFIX:-}caddy:2.10-alpine@sha256:4c6e91c6ed0e2fa03efd5b44747b625fec79bc9cd06ac5235a779726618e530d
|
||||||
profiles: [caddy]
|
profiles: [caddy]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
17
deploy/scripts/list-images.sh
Executable file
17
deploy/scripts/list-images.sh
Executable 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
|
||||||
@ -151,6 +151,64 @@ deliberately not digest-pinned.
|
|||||||
4. Verify after rollout: `docker inspect --format '{{.Image}}' <container>`
|
4. Verify after rollout: `docker inspect --format '{{.Image}}' <container>`
|
||||||
must print the pinned digest (or check `RepoDigests` on the image).
|
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
|
## 6. Verification checklist
|
||||||
|
|
||||||
- [ ] `https://test.dorfteich.cloud/healthz` → `ok`
|
- [ ] `https://test.dorfteich.cloud/healthz` → `ok`
|
||||||
|
|||||||
@ -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".
|
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
|
- [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
|
- [ ] Build ohne Netz reproduzierbar (pnpm Offline-Store / reine
|
||||||
Prebuilt-Images) · 2–3 AT · #219
|
Prebuilt-Images) · 2–3 AT · #219
|
||||||
- [ ] Testlauf in netzisolierter Umgebung, Protokoll als Beleg · 2 AT · #220
|
- [ ] Testlauf in netzisolierter Umgebung, Protokoll als Beleg · 2 AT · #220
|
||||||
|
|||||||
@ -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`
|
nach dieser Prozedur aufgesetzt und laufen produktiv (`deploy/stages.md`
|
||||||
dokumentiert die realen Instanzen).
|
dokumentiert die realen Instanzen).
|
||||||
|
|
||||||
**Airgap-/Offline-Variante:** ⏳ offen — Mirror-Verfahren (#218),
|
**Airgap-/Offline-Variante:** teilweise verfügbar —
|
||||||
netzloser Build (#219), Testlauf in isolierter Umgebung (#220),
|
|
||||||
Offline-Update-Pfad (#221); Meilenstein M28. Bereits vorhanden als
|
- **Mirror-Verfahren (#218): ✅ verfügbar und einmal end-to-end
|
||||||
Grundlage: alle Dritt-Images digest-gepinnt (#203), ein authoritativer
|
ausgeführt.** Schrittfolge: Image-Liste generieren
|
||||||
Node-Pin (#236), SBOMs je Release (#202).
|
(`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
|
## 2 Update und Rollback
|
||||||
|
|
||||||
|
|||||||
55
docs/vs-nfd/95-mirror-protokoll.md
Normal file
55
docs/vs-nfd/95-mirror-protokoll.md
Normal 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 1–3 aus §5b für die geänderten
|
||||||
|
Images; die Liste kommt IMMER frisch aus `list-images.sh`.
|
||||||
Loading…
Reference in New Issue
Block a user