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>
8.7 KiB
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.network→ caddy →: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 untracked — git checkout .env will wipe it. Holds:
PHOTO_DIRS=/mnt/library/images/NEXTCLOUD_USERS_HOST_PATH=/mnt/library/homecloudNEXTCLOUD_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/homecloudis bind-mounted intobackend,worker-light,worker-watcher,worker-visionas/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 inusers.nextcloud_app_password_enc) so Nextcloud'soc_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_usernameoverrides the default OIDCpreferred_username.dtoro(mule-image) maps toadmin(Nextcloud) — don't assume username equality.- Surviving SourceRoots in DB:
Photos→/nextcloud-users/admin/files/Photos;Memories→/nextcloud-users/admin/files/Memories(both owned bydtoro). Usermulihasnextcloud_username=mulibackfilled 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/deploy → mule-deploy-webhook.service:
- Validates HMAC against
/etc/mule-deploy/secret - Filters to
refs/heads/main - Runs
/opt/mule-deploy/deploy.shin 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.
Related
- Nextcloud (114) — source of truth for photo libraries
- Authentik (124)
- Caddy (121)
- DNS
- Auto-deploy
- Gitea (104)
Changelog
2026-05-10 — photos.hubris.network perf sweep
User reported slow load. Five fixes shipped, in order:
- Backend out of dev mode.
docker-compose.ymlcommand:was runninguvicorn … --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. - 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 PILImage.open(src_path)as the last fallback in both_generate_proxy_webp(routers/photos.py) andprocess_raw_image(tasks/thumbs.py). ~1,300 iPhone DNGs were 415-ing on every detail view; now decode in <1s via PIL. - Reconcile DB with disk (renamed-folder case).
prune_missing_photoswas skipping all 4,154 orphaned photo rows under SourceRootTaco and Muli - 2024 onward(renamed in Nextcloud toPhoto Archive 2004-2024) because the leaf path didn't resolve and the code assumed "drive unmounted → must skip". Added_sr_state()to classify aspresent/renamed(parent mount fine, leaf gone) /unmounted(parent inaccessible). Onlyunmountedstill skips. Two stale source roots logged with a clearer hint pointing atPOST /api/v1/library/maintenance/prune-missing. User has not yet been asked to run that — endpoint is ready when they are. - Frontend page size + idle polling.
usePhotosQuery.tswas first-fetchingper_page=500(slow paint + 500 thumb requests at once). Split intoPER_PAGE_INITIAL=100for first paint,PER_PAGE_BACKGROUND=500for the cursor-chain prefetch. Idle polling forscan-statusandworker-status(useScanActivity.ts,ScanProgress.tsx) bumped from 10s/15s to 30s/30s while idle; active cadence (2s/3s) unchanged. - Partial index on
photos. Default list queryWHERE NOT is_trashed AND NOT is_hidden ORDER BY taken_at DESC NULLS LAST, id DESC LIMIT Nwas doing a seq-scan + top-N heapsort (~25ms standalone, worse under concurrency). Added migration0017_photos_list_indexcreatingix_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 ~500–1,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.