#203: pin all third-party deploy images by digest #262

Merged
fable-5 merged 1 commits from feat/203-image-digest-pinning into main 2026-07-31 05:28:56 +02:00
5 changed files with 53 additions and 8 deletions

View File

@ -91,6 +91,22 @@ jobs:
fi
exit "$bad"
# Third-party deploy images are pinned by digest (issue #203): every
# image in the deploy compose that is not one of our own
# (${IMAGE_PREFIX}…) must carry @sha256 — the tag stays for
# readability, the digest decides what runs. Update procedure:
# deploy/stages.md §Third-party image digests. compose.dev.yml is a
# local convenience, deliberately not held to this.
- name: Third-party compose images are digest-pinned
run: |
set -euo pipefail
bad=$(grep -hE '^ *image: ' deploy/compose/docker-compose.yml | grep -v 'IMAGE_PREFIX' | grep -v '@sha256:' || true)
if [ -n "$bad" ]; then
echo "third-party image reference(s) without a digest:"
echo "$bad"
exit 1
fi
- name: Set up pnpm
uses: pnpm/action-setup@v4

View File

@ -14,7 +14,7 @@
services:
web:
image: node:22.15-alpine
image: node:22.15.1-alpine
build: !reset null
working_dir: /repo
command: sh -c "npm i -g pnpm@11 && pnpm install && pnpm --filter @dorfteich/web dev -- --host 0.0.0.0"
@ -33,7 +33,7 @@ services:
depends_on: !reset []
api:
image: node:22.15-alpine
image: node:22.15.1-alpine
build: !reset null
working_dir: /repo
command: sh -c "npm i -g pnpm@11 && pnpm install && pnpm --filter @dorfteich/shared build && pnpm --filter @dorfteich/api start:dev"
@ -53,7 +53,7 @@ services:
- pnpm-store:/root/.local/share/pnpm/store
collab:
image: node:22.15-alpine
image: node:22.15.1-alpine
build: !reset null
working_dir: /repo
command: sh -c "npm i -g pnpm@11 && pnpm install && pnpm --filter @dorfteich/shared build && pnpm --filter @dorfteich/collab start:dev"

View File

@ -193,7 +193,7 @@ services:
<<: *logging
db:
image: postgres:17.5-alpine
image: 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
image: 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
image: 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
image: caddy:2.10-alpine@sha256:4c6e91c6ed0e2fa03efd5b44747b625fec79bc9cd06ac5235a779726618e530d
profiles: [caddy]
restart: unless-stopped
ports:

View File

@ -122,6 +122,35 @@ The pipeline pushes images to the Gitea container registry
- On the host, `docker login gitea.101010.cloud` for the `deploy` user
with a `read:package` token, so `compose pull` works.
## 5a. Third-party image digests (issue #203, ADR 0024)
Every third-party image in `deploy/compose/docker-compose.yml` is pinned
as `name:tag@sha256:…` — the tag stays for readability, the digest
decides what runs, so the deployed artefact is exactly the reviewed one.
An early CI step fails on any third-party `image:` reference without a
digest. Our own images are pinned per release by the deploy pipeline
(`TAG` in the stage `.env`); `compose.dev.yml` is a local convenience and
deliberately not digest-pinned.
**Updating a digest** (e.g. to take a rebased base image or a new tag):
1. Resolve the new digest — this prints the manifest-list digest every
platform pulls:
```bash
docker buildx imagetools inspect <name:tag> # → Digest: sha256:…
```
2. Update the reference in `deploy/compose/docker-compose.yml` to
`<name:tag>@sha256:…` and let CI confirm.
3. **Roll out by hand**: CD does NOT sync stage composes — apply the same
change to `/srv/DOCKER/dorfteich-{test,int,prod}/docker-compose.yml`
on ONE. The next `compose pull && up -d` (any CD run for test/int, the
next release deploy for prod) recreates the containers from the pinned
digest.
4. Verify after rollout: `docker inspect --format '{{.Image}}' <container>`
must print the pinned digest (or check `RepoDigests` on the image).
## 6. Verification checklist
- [ ] `https://test.dorfteich.cloud/healthz``ok`

View File

@ -69,7 +69,7 @@ Digest-Pinning (#203) läuft vorgezogen in `M25`_
Hochgezogen, weil das eine Frage im **ersten** Behördengespräch ist. „Sollte
gehen" ist dort eine schlechtere Antwort als „getestet, hier ist die Anleitung".
- [ ] 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
- [ ] Build ohne Netz reproduzierbar (pnpm Offline-Store / reine
Prebuilt-Images) · 23 AT · #219