Files
oikos/containers/127-mule-photos-new.md

7.8 KiB

127 — mule-photos-new

Side-by-side PhotoPrism M0 test of the dtoro/mule-image new branch at photos-new.hubris.network. Production LXC 120 keeps running on the legacy stack at photos.hubris.network until M5 cutover.

At a glance

  • Hostname: mule-photos-new
  • IP: 192.168.8.181
  • Privilege: unpriv
  • Resources: 6 cores / 8 GiB RAM / 40 GiB rootfs / 1 GiB swap
  • Features: nesting=1,fuse=1,keyctl=1
  • Mounts: (none — see scratch copy below)
  • Public hostname: photos-new.hubris.networkcaddy (121) → split (PhotoPrism :2342, sidecar :8000, Vite :5173)

Stack (/opt/mule-image)

/opt/mule-image is the working tree of dtoro/mule-image on branch new. Compose lives at docker-compose.photoprism.yml; LXC-127-only overrides at docker-compose.photoprism.override.yml (untracked — see Why an override exists). Invoked with --env-file .env.photoprism.

Service Container Port Notes
mariadb pp-mariadb 127.0.0.1:3306 MariaDB 11; named volume pp_mariadb_data; init SQL provisions mule_sidecar.marks table
photoprism pp-app :2342 docker.io/photoprism/photoprism:latest, runs as uid 33 (PP_UID/GID env)
sidecar pp-sidecar :8000 (overridden) Go + Gin service for rename / folder mutations / heap convert / dup detect
vite (host) systemd unit :5173 SvelteKit dev server (mule-vite.service), npm run dev in /opt/mule-image/web

PhotoPrism is enterprise-tier (tier: 1) per the build tag (-Plus). TF vision pipeline and EXIF backwrite are disabled in M0 — PP_READONLY=true keeps the originals view read-only as the M0 safety net.

Library — writable rsync scratch copy (NOT the real admin Photos)

Unlike LXC 120 (which mounts /mnt/library directly), this LXC has no bind-mount of the production library. Instead a one-shot rsync of the admin's Photos lives on the LXC's own rootfs:

  • Host source (read-only reference): /mnt/library/homecloud/admin/files/Photos (~4.9 GB)
  • LXC scratch (writable): /srv/photos-scratch (owner www-data:media, mode 0775)

This means sidecar rename / folder mutation operations land in the scratch copy, not the real admin library. The scratch is not auto-synced — it's a snapshot from 2026-05-17. To refresh from production:

# on hubris (LXC 127 must be stopped to mount its rootfs)
pct stop 127
pct mount 127
rsync -aHAX --info=stats2 --chown=100033:110000 --no-perms \
  --chmod=Du=rwx,Dg=rx,Do=rx,Fu=rw,Fg=r,Fo=r \
  /mnt/library/homecloud/admin/files/Photos/ \
  /var/lib/lxc/127/rootfs/srv/photos-scratch/
pct unmount 127
pct start 127

--chown=100033:110000 accounts for the unprivileged-LXC ID shift (host 100033 = LXC www-data, host 110000 = LXC media). Don't try a bind-mount of /mnt/library/... — the admin Photos tree is 0750 and unprivileged LXCs can't see through.

Auth — Authentik OIDC

PhotoPrism's "Sign in with OIDC" button delegates to Authentik (124).

  • Provider/Application slug: mule-photos-new
  • Issuer: https://auth.hubris.network/application/o/mule-photos-new/
  • Redirect URI: https://photos-new.hubris.network/api/v1/oidc/redirect
  • Scopes: openid profile email
  • Initiated by clicking the OIDC button at /library/loginGET /api/v1/oidc/login → 302 to Authentik authorize.
  • OIDC_REGISTER=true + OIDC_ROLE=admin so the first SSO login auto-creates a PhotoPrism admin account.

Local PhotoPrism admin (username admin, password in /root/mule-photos-new-secrets.txt on hubris) stays available as a fallback.

Why an override exists

docker-compose.photoprism.override.yml is only on LXC 127 (not in the git repo) and pins two M0-era inconsistencies from the upstream compose file:

  1. Sidecar bind address. Upstream binds sidecar to 127.0.0.1:8000 because the M4 design colocates Caddy with the sidecar. On this test LXC Caddy lives on a different host (LXC 121), so the override binds the sidecar port to 0.0.0.0:8000.
  2. OIDC env-var names. Upstream passes PHOTOPRISM_OIDC_ISSUER_URL / _CLIENT_ID / _CLIENT_SECRET / _PROVIDER_NAME, but PhotoPrism actually reads PHOTOPRISM_OIDC_URI / _CLIENT / _SECRET / _PROVIDER. The override re-maps. Confirmed live with docker exec pp-app photoprism show config.

Both fixes should land upstream on the new branch; once they do the override file becomes dead code and can be removed.

Auto-deploy

Mirrors the LXC 120 pattern.

  • Webhook listener: mule-deploy-webhook.servicepython3 /opt/mule-deploy/webhook.py on 0.0.0.0:9797.
  • Branch filter: refs/heads/new (LXC 120 still owns main).
  • HMAC secret: /etc/mule-deploy/secret (mode 0600).
  • Deploy script: /opt/mule-deploy/deploy.shgit fetch && git reset --hard origin/new, fix pp/{storage,import} ownership to 33:10000 (PP container uid), docker compose ... up -d --build --force-recreate with both compose files, systemctl restart mule-vite.
  • Gitea webhook id 9 on dtoro/mule-image pointed at http://192.168.8.181:9797/deploy.

Push to the new branch on git.hubris.network/dtoro/mule-image → webhook fires → rebuild. The legacy LXC 120 watches main and is unaffected.

Bootstrap secrets

Saved on hubris at /root/mule-photos-new-secrets.txt (mode 0600):

  • PP_ADMIN_PASSWORD — initial PhotoPrism admin login
  • PP_DB_PASSWORD — MariaDB photoprism user
  • PP_DB_ROOT_PASSWORD — MariaDB root
  • OIDC_CLIENT_ID / OIDC_CLIENT_SECRET — generated by ak shell against Authentik

SIDECAR_DB_PASSWORD is still the literal placeholder replace-at-m4-bringup because mariadb/init/01-sidecar.sql hardcodes it; rotate before this stack ever goes public.

Health checks

# from hubris
pct exec 127 -- curl -sf http://127.0.0.1:2342/api/v1/status        # PP
pct exec 127 -- curl -sf http://127.0.0.1:8000/api/sidecar/healthz  # sidecar
pct exec 127 -- curl -sf http://127.0.0.1:5173/                     # Vite

# through Caddy
curl -sk --resolve photos-new.hubris.network:443:192.168.8.175 \
  https://photos-new.hubris.network/api/v1/oidc/login -i | head -2   # 302 → auth.hubris.network

Changelog

2026-05-17 — Bring-up

LXC 127 created from debian-13-standard_13.1-2, joined to vmbr0 with static IP 192.168.8.181. Docker engine + Node 20 installed.

dtoro/mule-image cloned at branch new, compose stack (docker-compose.photoprism.yml) brought up: MariaDB 11 + PhotoPrism :latest (-Plus build) + Go sidecar (built locally). Vite dev server running as mule-deploy via mule-vite.service on port 5173.

Authentik OIDC application mule-photos-new provisioned via ak shell (OAuth2Provider + Application + STRICT RedirectURI). PhotoPrism's OIDC button delegates to Authentik; OIDC_REGISTER=true / OIDC_ROLE=admin so the first SSO login becomes admin.

Admin's Photos library rsynced (~4.9 GB, 1206 files) into /srv/photos-scratch on the LXC rootfs (no bind-mount). Sidecar mutations land in the scratch copy, not the real library.

Caddy site photos-new.hubris.network added in dtoro/caddy-conf; dnsmasq entry on LXC 124 → 192.168.8.175. dnsmasq required a restart (not reload) for the new address= line to take effect.

docker-compose.photoprism.override.yml (LXC-only, untracked) pins two upstream issues: sidecar bound to 127.0.0.1 (cross-host Caddy can't reach), and OIDC env-var name mismatch (PHOTOPRISM_OIDC_ISSUER_URL vs PHOTOPRISM_OIDC_URI and friends). Should land upstream on new next iteration.