feat(immich): manifest-driven stack via orchestrator — live-migrated on .228

Completes the immich migration off the legacy hardcoded install_immich_stack
(podman run + sudo chown) to the registry-manifest + orchestrator path. Validated
live on .228 (clean single set, healthy v2.7.4, data dir ownership correct).

- install_immich_stack now tries install_stack_via_orchestrator(immich_stack_app_ids)
  first; legacy remains only as the no-manifests fallback.
- immich-{postgres,redis,server} manifests corrected from live findings:
  * named by app_id (dropped container_name override) — using container_name
    spawned DUPLICATE containers (app_id-named install vs name-override reconcile)
    on the same PGDATA, which corrupted a postgres cluster. Server reaches its
    siblings via app_id aliases (DB_HOSTNAME=immich-postgres, REDIS=immich-redis).
  * immich-postgres data_uid 100998:100998 (postgres drops to container 999 →
    host 100998 under rootless; verified the fresh dir is chowned correctly).
  * immich-server version "release"→"2.7.4" (manifest validation requires a digit;
    the bad version made the manifest silently skip → partial orchestrator install
    → legacy fallback → the duplicate corruption above).
- HARDEN install_stack_via_orchestrator: only fall back to the legacy installer
  when NOTHING was installed yet. An "unknown app_id" AFTER a member is up now
  errors instead of double-creating containers on shared data (the corruption
  root cause).
- Strict the all-manifests round-trip test: fail (not skip) on any invalid shipped
  manifest — this gap let the bad immich-server version through.

Known follow-up (pre-existing, platform-wide): orchestrator-installed backends
(immich, btcpay-db) run as podman --restart, not Quadlet, and podman-restart.service
is disabled on .228 → reboot-survival gap independent of this migration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-06-21 07:08:45 -04:00
co-authored by Claude Opus 4.8
parent 011081d180
commit 9e6c5370fc
6 changed files with 61 additions and 18 deletions
@@ -3778,10 +3778,14 @@ app:
if !mf.exists() {
continue;
}
let m = match AppManifest::from_file(&mf) {
Ok(m) => m,
Err(_) => continue, // a malformed disk manifest is a separate concern
};
// Every shipped manifest MUST be valid. load_manifests() silently
// skips malformed ones in prod, which once let an invalid app.version
// ("release", no digit) ship — the app then vanished from the
// orchestrator and a stack install half-fell-back to the legacy path.
// Fail loudly here instead.
let m = AppManifest::from_file(&mf).unwrap_or_else(|e| {
panic!("shipped manifest {} must be valid: {e}", mf.display())
});
let id = m.app.id.clone();
let is_build = m.app.container.build.is_some();
let value = serde_json::to_value(&m).expect("manifest serializes to JSON");