upstream image drift audit — app-catalog-felhom.eu — 2026-09-21
======================================================================

Read-only measurement, run from DooPlex against public registries only
(Docker Hub, ghcr.io, lscr.io, registry.gitlab.com). Nothing was queried
from a customer/demo box. The catalog repo was not modified and nothing
was committed.

PRE-EXISTING REPO STATE NOTE: at the start of this session `git status
--porcelain` in app-catalog-felhom.eu already showed 3 modified files
(CLAUDE.md, scripts/check-engine-major.py, scripts/test_gate_decoys.py) —
an in-progress, uncommitted R-469 change (MariaDB major-upgrade gate)
that predates this session. This audit did not touch those files and
made no edits/commits anywhere in the repo; flagging only because the
task said 'do not modify' and the tree was already not clean.

1. INVENTORY
----------------------------------------------------------------------
Apps (templates/*/docker-compose.yml): 53
Total `image:` lines across all composes: 79
Unique image refs (pins): 66

  floating (DB-engine family tag, e.g. postgres:16-alpine, mariadb:11.6,
            redis:7-alpine, postgis:16-3.5-alpine): 8
  pinned-semver (full app release tag): 58

App classification (from compose + .felhom.yml, not memory):
  database = has a postgres/mariadb/mysql/mongo sidecar image
  file-leg = binds host media/library paths (HDD_PATH / USERDATA_PATH),
             no DB sidecar
  other    = neither
  database: 14 apps -> bookstack, calcom, claper, docmost, immich, kimai, nextcloud, outline, paperless-ngx, rallly, romm, sparkyfitness, tandoor, zipline
  file-leg: 9 apps -> audiobookshelf, calibre-web, emby, jellyfin, komga, navidrome, plex, radarr, sonarr
  other: 30 apps -> actualbudget, adventurelog, bentopdf, code-server, crafty-controller, ghost, gitea, glance, gokapi, grafana, gramps-web, home-assistant, homebox, homepage, mealie, n8n, onlyoffice, opengist, papra, plant-it, privatebin, recipe-importer, seerr, termix, uptime-kuma, vaultwarden, vikunja, wanderer, wger, wishlist

EDGE CASE flagged, not silently folded in: `adventurelog` runs a
postgis/postgis (postgres-derivative) DB sidecar, but the string
'postgis' does not contain 'postgres'/'mariadb'/'mysql'/'mongo', so by
the literal substring rule it lands in 'other', not 'database'. If you
want postgis apps folded into 'database', adventurelog is the one app
that moves (other: 30->29, database: 14->15).

2. DEAD-IMAGE CHECK (does the pin still resolve at all)
----------------------------------------------------------------------
Ran the repo's own gate, unmodified, exactly as documented in CLAUDE.md:
  $ python3 scripts/check-image-resolvable.py --all
Result: 65 of 66 pins verified via `docker manifest inspect`, ALL resolve.
NO dead images among those checked.

The 1 unchecked: msdeluise/plant-it:0.10.0 (app `plant-it`,
lifecycle: abandoned). The gate calls it INCONCLUSIVE (rc=0 but
stderr contains 'denied'/'unauthorized', which its own trap-1 logic
refuses to read as success). Independently: hub.docker.com's own
catalog API returns {"message":"object not found"} for the whole
msdeluise/plant-it repository (not just the tag), and the upstream
GitHub repo msdeluise/plant-it now 404s too — two positive signals the
image is gone, stronger than the registry gate alone captures, but I'm
stating both rather than asserting 'dead' from a registry API that
itself only said INCONCLUSIVE. This matches its already-documented
`lifecycle: abandoned` state — expected, not a new incident.

3. PINNED-SEMVER: IS THERE A NEWER TAG UPSTREAM
----------------------------------------------------------------------
pinned-semver pins evaluated: 58
  behind: 46
  no-newer-or-unparsed: 10
  error: 1
  internal-not-upstream: 1

Of the 46 pins with a newer upstream tag: 7 cross a MAJOR version,
39 are MINOR/PATCH steps (same leading version number).

NOTE ON THE MAJOR/MINOR RULE: 'major' = the first dot-separated numeric
component changed. This is literal, not semantic — it correctly reads a
real major bump for apps like nextcloud/claper/paperless-ngx, but for
calendar-versioned images (home-assistant: YYYY.MM.PATCH,
actualbudget: YY.MM.PATCH) the 'first number' is a year, so a same-year
update is correctly minor/patch and would only misread as 'major' at a
year rollover — noted, not a defect found in this sweep's data.

4. APPS BEHIND ON A PINNED-SEMVER IMAGE, sorted furthest-behind first
----------------------------------------------------------------------
STEP        IMAGE REPO                                          CURRENT               NEWEST                APP(S)              PINNED SINCE
MAJOR       nextcloud                                           34.0.1-apache         35.0.0-apache         nextcloud           2026-07-18
MAJOR       f0rc3/gokapi                                        v1.9.6                v2.2.4                gokapi              2026-07-18
MAJOR       ghcr.io/gethomepage/homepage                        v1.13.2               v2.4.0                homepage            2026-07-18
MAJOR       codewithcj/sparkyfitness_server                     v0.17.3               v1.7.2                sparkyfitness       2026-07-18
MAJOR       codewithcj/sparkyfitness                            v0.17.3               v1.7.2                sparkyfitness       2026-07-18
MAJOR       ghcr.io/paperless-ngx/paperless-ngx                 2.20.15               3.2.1                 paperless-ngx       2026-07-18
MAJOR       ghcr.io/claperco/claper                             2.5                   3.0                   claper              2026-07-18
minor/patch plexinc/pms-docker                                  1.41.4.9463-630c9f557 -> 1.43.4.10903-e5521bd8c   plex                2026-02-15
minor/patch emby/embyserver                                     4.10.0.20             4.11.0.1              emby                2026-07-18
minor/patch getmeili/meilisearch                                v1.36.0               v1.54.0               wanderer            2026-07-21
minor/patch ghost                                               6.53.0-alpine         6.64.0-alpine         ghost               2026-07-18
minor/patch n8nio/n8n                                           2.31.3                2.40.4                n8n                 2026-07-18
minor/patch lscr.io/linuxserver/code-server                     4.129.0               4.138.0               code-server         2026-07-18
minor/patch ghcr.io/mealie-recipes/mealie                       v3.20.1               v3.27.0               mealie              2026-07-18
minor/patch lukevella/rallly                                    4.11.1                4.15.1                rallly              2026-07-18
minor/patch ghcr.io/lukegus/termix                              2.5.0                 2.8.0                 termix              2026-07-12
minor/patch vikunja/vikunja                                     2.3.0                 2.6.0                 vikunja             2026-07-18
minor/patch ghcr.io/home-assistant/home-assistant               2026.7.2              2026.9.3              home-assistant      2026-07-18
minor/patch actualbudget/actual-server                          26.7.0                26.9.0                actualbudget        2026-07-18
minor/patch gotson/komga                                        1.25.0                1.27.0                komga               2026-07-18
minor/patch rommapp/romm                                        5.0.0                 5.2.0                 romm                2026-07-18
minor/patch ghcr.io/immich-app/immich-server                    v3.0.3                v3.2.2                immich              2026-07-18
minor/patch ghcr.io/immich-app/immich-machine-learning          v3.0.3                v3.2.2                immich              2026-07-18
minor/patch louislam/uptime-kuma                                2.4.0                 2.5.5                 uptime-kuma         2026-07-18
minor/patch lscr.io/linuxserver/radarr                          6.3.0                 6.4.4                 radarr              2026-07-18
minor/patch vaultwarden/server                                  1.36.0-alpine         1.37.3-alpine         vaultwarden         2026-07-18
minor/patch grafana/grafana                                     13.1.0                13.2.2                grafana             2026-07-18
minor/patch ghcr.io/advplyr/audiobookshelf                      2.35.1                2.36.1                audiobookshelf      2026-07-18
minor/patch docmost/docmost                                     0.95.0                0.96.0                docmost             2026-07-18
minor/patch outlinewiki/outline                                 1.9.1                 1.10.1                outline             2026-07-18
minor/patch ghcr.io/cmintey/wishlist                            v0.66.0               v0.67.0               wishlist            2026-07-19
minor/patch ghcr.io/seanmorley15/adventurelog-backend           v0.12.1               v0.13.0               adventurelog        2026-07-18
minor/patch ghcr.io/seanmorley15/adventurelog-frontend          v0.12.1               v0.13.0               adventurelog        2026-07-18
minor/patch ghcr.io/diced/zipline                               4.6.1                 4.7.0                 zipline             2026-07-18
minor/patch deluan/navidrome                                    0.63.2                0.64.0                navidrome           2026-07-18
minor/patch registry.gitlab.com/crafty-controller/crafty-4      4.10.7                4.11.0                crafty-controller   2026-06-26
minor/patch lscr.io/linuxserver/bookstack                       26.05.2               26.05.5               bookstack           2026-07-18
minor/patch gitea/gitea                                         1.27.0                1.27.3                gitea               2026-07-18
minor/patch ghcr.io/alam00000/bentopdf                          v2.8.6                v2.8.8                bentopdf            2026-07-12
minor/patch ghcr.io/thomiceli/opengist                          1.13                  1.15                  opengist            2026-07-18
minor/patch ghcr.io/tandoorrecipes/recipes                      2.6.13                2.6.15                tandoor             2026-07-18
minor/patch glanceapp/glance                                    v0.8.5                v0.8.6                glance              2026-07-18
minor/patch ghcr.io/papra-hq/papra                              26.6.1-rootless       26.6.2-rootless       papra               2026-07-12
minor/patch privatebin/pdo                                      2.0.5                 2.0.6                 privatebin          2026-09-14
minor/patch lscr.io/linuxserver/sonarr                          4.0.19                4.0.20                sonarr              2026-07-18
minor/patch wger/server                                         2.6                   2.7                   wger                2026-07-19

5. BY APP CLASS
----------------------------------------------------------------------
database (14 apps, 23 unique pins):
  floating: 7   pinned-semver: 16
  of pinned-semver -> behind: 14 (major: 5, minor/patch: 9), up to date: 2, other: 0

file-leg (9 apps, 9 unique pins):
  floating: 0   pinned-semver: 9
  of pinned-semver -> behind: 7 (major: 0, minor/patch: 7), up to date: 2, other: 0

other (30 apps, 34 unique pins):
  floating: 1   pinned-semver: 33
  of pinned-semver -> behind: 25 (major: 2, minor/patch: 23), up to date: 6, other: 2

6. FLOATING PINS — has the tag's digest moved since the catalog set it
----------------------------------------------------------------------
Method: compare each floating tag's registry-reported last-push date
(Docker Hub API tag metadata; ghcr.io has no equivalent public
timestamp API reachable anonymously) against the app's own
`catalog_since` (the date THIS repo last moved that pin — CLAUDE.md's
own definition of 'when we set it').

  mariadb:11.4                        apps=romm                 catalog_since=2026-07-18  registry last push=2026-09-18T02:36:29.445608Z
  mariadb:11.6                        apps=kimai, nextcloud     catalog_since=2026-07-18  registry last push=2025-02-04T13:35:57.325706Z
  mariadb:12.3                        apps=bookstack            catalog_since=2026-07-18  registry last push=2026-09-18T02:37:26.402424Z
  postgres:15-alpine                  apps=sparkyfitness        catalog_since=2026-07-18  registry last push=2026-09-21T07:07:40.622579Z
  postgres:16-alpine                  apps=8 apps (see below)   catalog_since=2026-07-18  registry last push=2026-09-21T04:07:46.010722Z
  redis:7-alpine                      apps=6 apps (see below)   catalog_since=2026-07-18  registry last push=2026-09-18T03:04:58.155237Z
  postgis/postgis:16-3.5-alpine       apps=adventurelog         catalog_since=2026-07-18  registry last push=2026-08-31T11:37:24.0266Z

Verdict (comparing dates above): 6 of 7 Docker-Hub-hosted floating pins
have had their tag repushed (new digest) at least once since the catalog
set them (mariadb:11.4, mariadb:12.3, postgres:15-alpine,
postgres:16-alpine, redis:7-alpine, postgis:16-3.5-alpine — pushes on or
after 2026-08-31, vs. catalog_since 2026-07-18 for all of them). Only
mariadb:11.6 (kimai, nextcloud) has NOT been repushed since pinning —
its last Docker Hub push was 2025-02-04, well before catalog_since.

ghcr.io/immich-app/postgres:16-vectorchord0.4.3-pgvectors0.2.0 (immich):
COULD NOT TELL. ghcr.io's anonymous v2 API exposes no last-modified
timestamp, and I did not have an authenticated GitHub Packages token to
query the versions API. Digest resolved today:
  sha256:1a078b237c1d9b420b0ee59147386b4aa60d3a07a8e6a402fc84a57e41b043a4
but I have no earlier digest recorded anywhere to compare it to, so
'has it moved since catalog_since' is unmeasured for this one pin —
stated as unmeasured rather than guessed.

7. REPRODUCIBLE COMMANDS
----------------------------------------------------------------------
Dead-image check (the repo's own gate, unmodified):
  cd app-catalog-felhom.eu && python3 scripts/check-image-resolvable.py --all

Newer-tag-upstream check (this audit's script, not part of the repo,
kept in the scratchpad — reuses the registry-v2 anonymous-bearer-token
auth flow from scripts/check-image-resolvable.py, but lists the full
upstream tag set per repo instead of testing one ref):
  python3 /tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/5f83c0fb-b1bf-404e-b99e-2e3b28a31615/scratchpad/drift.py
  (writes results.json next to it; apps.json/pins.json are the inventory
  extracted from the catalog's compose/.felhom.yml files)

Floating-tag last-push check (Docker Hub API only, no docker CLI):
  curl -s https://hub.docker.com/v2/repositories/<ns>/<repo>/tags/<tag>
