Files
oikos/containers/120-mule-images.md
Claudio 9b795f0489 docs(mule-image): photos.hubris.network perf sweep changelog
Five fixes this session: backend out of dev mode, PIL fallback for
iPhone Apple ProRAW Linear DNG, source root state classification so
prune-missing handles renamed folders, smaller initial page size +
idle polling backoff, and a partial index on photos that takes the
list query from 25ms seq-scan to 0.1ms index-only scan.

Also flags the deploy.sh --force-recreate gap that bit twice this
session and the outstanding data-drift (4,154 orphan rows + one
unregistered folder).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:30:38 +02:00

8.7 KiB
Raw Blame History

120 — mule-images

Hosts mule-image / "mulita" — the photos app at photos.hubris.network. Auto-deploys from dtoro/mule-image on git push origin main.

At a glance

  • Hostname: mule-images
  • IP: 192.168.8.136
  • Privilege: privileged
  • Resources: 4 cores / 8 GiB RAM / 60 GiB rootfs
  • Mounts: /mnt/library/mnt/library
  • Public hostname: photos.hubris.networkcaddy:3000 (frontend)

Stack (/opt/mule-image)

/opt/mule-image IS the working tree of dtoro/mule-image. Compose at /opt/mule-image/docker-compose.yml. Services:

Service Port Notes
frontend 3000 Reverse-proxied by Caddy
backend 8001 FastAPI
worker-vision ML scan worker
worker-light Lightweight worker
worker-watcher FS watcher
db (pg) pgvector
redis (rd) queue

.env is untrackedgit checkout .env will wipe it. Holds:

  • PHOTO_DIRS=/mnt/library/images/
  • NEXTCLOUD_USERS_HOST_PATH=/mnt/library/homecloud
  • NEXTCLOUD_BASE_URL=https://cloud.hubris.network
  • OIDC client secret + scopes
  • SECRET_KEY (generated)

Nextcloud-rooted libraries (since 2026-04-26)

Photo libraries live under each user's Nextcloud files/ tree, NOT in /mnt/library/images/*.

  • /mnt/library/homecloud is bind-mounted into backend, worker-light, worker-watcher, worker-vision as /nextcloud-users. Each NC user is /nextcloud-users/<nc_user>/files/.
  • Reads use that bind directly.
  • Mutations (upload, delete, rename, move) dispatch through services/nextcloud_dav.py (HTTP Basic auth, per-user app password Fernet-encrypted in users.nextcloud_app_password_enc) so Nextcloud's oc_filecache, trashbin, comments, and desktop-sync clients stay coherent.
  • Photo copy + cross-system moves return 501 with a "use Nextcloud's web UI" hint — defer until needed.
  • users.nextcloud_username overrides the default OIDC preferred_username. dtoro (mule-image) maps to admin (Nextcloud) — don't assume username equality.
  • Surviving SourceRoots in DB: Photos/nextcloud-users/admin/files/Photos; Memories/nextcloud-users/admin/files/Memories (both owned by dtoro). User muli has nextcloud_username=muli backfilled but no SourceRoot yet.
  • Pre-migration DB dump: /root/snapshots/mulita-pre-nc-migration-20260426-075132.dump (11 MB) on the host.

Authentication (since 2026-04-22)

Native OIDC via Authentik. Code in backend/app/auth_oidc.py, routes /api/v1/auth/oidc/{login,callback}. Authentik side:

  • OAuth2/OIDC Provider, client ID fCuHew48ONTskDjUKnMTZjFbVXuHwvQqTScQRNQ1
  • App slug mule-image
  • Redirect URI: https://photos.hubris.network/api/v1/auth/oidc/callback

Backend container needs extra_hosts: auth.hubris.network:192.168.8.175 via docker-compose.override.yml (gitignored). Otherwise Authlib's metadata fetch fails with SSL: CERTIFICATE_VERIFY_FAILED: self-signed certificate (it ends up at a random public host because LXC DNS resolves the public IONOS A record).

Caddyfile stays plain reverse_proxy 192.168.8.136:3000 — no forward-auth, no /api/* bypass needed.

Auto-deploy

Push to dtoro/mule-image main → gitea webhook → http://192.168.8.136:9797/deploymule-deploy-webhook.service:

  • Validates HMAC against /etc/mule-deploy/secret
  • Filters to refs/heads/main
  • Runs /opt/mule-deploy/deploy.sh in a daemon thread (returns 202 immediately — docker builds exceed gitea's request timeout)
  • git pull --ff-only + docker compose up -d --build + docker image prune -f

Deploy tooling is outside the app repo: /opt/mule-deploy/{deploy.sh,webhook.py}, secret at /etc/mule-deploy/secret, unit at /etc/systemd/system/mule-deploy-webhook.service. Same shape as the Caddy + Artifacto pipelines. Gitea webhook id 6.

app.ini ALLOWED_HOST_LIST on gitea includes 192.168.8.136.

Logs: pct exec 120 -- journalctl -u mule-deploy-webhook -f. Manual deploy: pct exec 120 -- /opt/mule-deploy/deploy.sh.

For pushes from inside the LXC, gitea creds at /etc/mule-deploy/git-credentials (mode 600) — same token as /etc/caddy-deploy/git-credentials on caddy.

Changelog

2026-05-10 — photos.hubris.network perf sweep

User reported slow load. Five fixes shipped, in order:

  1. Backend out of dev mode. docker-compose.yml command: was running uvicorn … --reload (single worker, file-watcher). Page loads fan out ~15 parallel API calls; they all serialized on one event loop. → --workers 2 --proxy-headers. Two uvicorn worker procs now.
  2. iPhone Apple ProRAW / Linear DNG decode fixed. rawpy 0.26.1 + LibRaw 0.22 rejects Apple Linear DNG (Photometric Interpretation: Linear Raw, 8-bit) as "Unsupported file format or not RAW file". Those files are TIFF containers with developed RGB inside and have no embedded preview to fall back to. Added PIL Image.open(src_path) as the last fallback in both _generate_proxy_webp (routers/photos.py) and process_raw_image (tasks/thumbs.py). ~1,300 iPhone DNGs were 415-ing on every detail view; now decode in <1s via PIL.
  3. Reconcile DB with disk (renamed-folder case). prune_missing_photos was skipping all 4,154 orphaned photo rows under SourceRoot Taco and Muli - 2024 onward (renamed in Nextcloud to Photo Archive 2004-2024) because the leaf path didn't resolve and the code assumed "drive unmounted → must skip". Added _sr_state() to classify as present / renamed (parent mount fine, leaf gone) / unmounted (parent inaccessible). Only unmounted still skips. Two stale source roots logged with a clearer hint pointing at POST /api/v1/library/maintenance/prune-missing. User has not yet been asked to run that — endpoint is ready when they are.
  4. Frontend page size + idle polling. usePhotosQuery.ts was first-fetching per_page=500 (slow paint + 500 thumb requests at once). Split into PER_PAGE_INITIAL=100 for first paint, PER_PAGE_BACKGROUND=500 for the cursor-chain prefetch. Idle polling for scan-status and worker-status (useScanActivity.ts, ScanProgress.tsx) bumped from 10s/15s to 30s/30s while idle; active cadence (2s/3s) unchanged.
  5. Partial index on photos. Default list query WHERE NOT is_trashed AND NOT is_hidden ORDER BY taken_at DESC NULLS LAST, id DESC LIMIT N was doing a seq-scan + top-N heapsort (~25ms standalone, worse under concurrency). Added migration 0017_photos_list_index creating ix_photos_list_visible (partial index on the sort key, restricted to visible rows). EXPLAIN now shows an Index Only Scan → 24.7ms → 0.097ms, ~250× speedup.

Deploy gotcha (repeated). The docker compose up -d --build in deploy.sh does not recreate the backend when only runtime config (command: or env-only changes) or migration files change without changing the Dockerfile-bake outcome. Forced recreation with docker compose up -d --no-deps --force-recreate backend twice this session. Action item: add --force-recreate (or at least --force-recreate backend when backend files changed) to /opt/mule-deploy/deploy.sh so future deploys are reliable.

Data drift still outstanding. 4,154 photo rows + 1 unregistered folder (Photo Archive 2004-2024) on disk that's not a SourceRoot. The reconcile endpoints now work — user decides when to call them. The new folder needs to be added as a SourceRoot via the Settings UI before its files will be indexed.

Proxy cache still empty (mule-image_proxies_data volume is 4 KB). Pre-generating ~5001,500 WebP proxies for non-web-safe formats would make first-open of every RAW/HEIC photo instant. Deferred — needs a one-shot script and the disk-space tradeoff isn't worth it until the data-drift reconcile happens first.

2026-04-28 — wiki entry created

Initial documentation.

2026-04-26 — Nextcloud-rooted libraries shipped

Bind /mnt/library/homecloud into the workers, reads via filesystem, writes via WebDAV. users.nextcloud_username override field added; dtoro → admin mapping. Surviving SourceRoots cleaned up to NC paths.

2026-04-22 — native OIDC via Authentik

Authlib-based code in backend/app/auth_oidc.py. extra_hosts override for auth.hubris.network in compose override (gitignored).

2026-04-21 — auto-deploy pipeline shipped

Webhook receiver at :9797, async deploy returning 202. Mirrors caddy-conf / gitea-customizations.