# mule-image Self-hosted photo management built on top of [PhotoPrism][pp]. A SvelteKit frontend ([`web/`](web/)) plus a small Go service ([`sidecar/`](sidecar/)) fill in the keyboard-driven UI and the file/folder/mark endpoints PhotoPrism's REST API does not expose. PhotoPrism itself handles indexing, originals, thumbnails, and the database; we never re-implement those. ## Architecture ```text ┌──────────────────┐ /api/v1/* ┌──────────────┐ │ SvelteKit web/ │ ───────────────▶ │ photoprism │ ──▶ mariadb │ (Vite : 5173) │ /api/sidecar/* │ :2342 │ │ │ ─────────┐ └──────────────┘ └──────────────────┘ ▼ ┌──────────────┐ │ sidecar │ ──▶ mariadb (mule_sidecar.*) │ :8000 │ ──▶ originals FS (rename / folders / dups) └──────────────┘ ``` Three compose services — `mariadb`, `photoprism`, `sidecar` — plus the SvelteKit `web/` app served separately. PhotoPrism's port `2342` is **bound to `127.0.0.1` only**; it isn't a user-facing surface. The SvelteKit app is. What the sidecar adds on top of PhotoPrism (full list in [`sidecar/README.md`](sidecar/README.md)): - Per-photo marks (rating + color) persisted to `mule_sidecar.marks` - File rename + folder create/rename/delete with PhotoPrism reindex - Heap (album) → folder conversion - Perceptual-hash duplicate scan + archive ## Quick start ```bash cp .env.example .env # edit .env: set PHOTO_DIRS to the host path holding your library # rotate PP_ADMIN_PASSWORD, PP_DB_PASSWORD, PP_DB_ROOT_PASSWORD # before any non-local deployment. podman-compose --env-file .env \ -f docker-compose.yml \ -f docker-compose.podman.yml \ up -d ``` Then serve the frontend. For local use the simplest path is the Vite dev server: ```bash cd web npm install npm run dev # open http://localhost:5173 ``` For a static deployment, `npm run build` produces a bundle under `web/build/` that any static file host (nginx, Caddy, GitHub Pages-style) can serve. Reverse-proxy `/api/v1/*` to `http://127.0.0.1:2342` and `/api/sidecar/*` to `http://127.0.0.1:8000`. PhotoPrism's own UI is still reachable from the host at `http://127.0.0.1:2342` if you need admin features (user management, settings) — set up an SSH tunnel from your laptop if the server is remote. ## Configuration All knobs live in [`.env.example`](.env.example). The required ones: | Variable | Notes | |----------------------|-----------------------------------------------------------------------------------------------| | `PHOTO_DIRS` | Host path mounted at `/photoprism/originals`. The library. | | `PP_ADMIN_PASSWORD` | First-boot admin password. Rotate. | | `PP_DB_PASSWORD` | MariaDB password for the `photoprism` user. Rotate. | | `PP_DB_ROOT_PASSWORD`| MariaDB root password. Rotate. | | `PP_UID` / `PP_GID` | Host UID/GID that owns `PHOTO_DIRS`. PhotoPrism + sidecar drop to this user inside. | | `PP_PORT` | Loopback host port for PhotoPrism (default `2342`). | | `PP_ORIGINALS_MODE` | `rw` (default) or `ro` — see [Read-only libraries](#read-only-libraries). | | `SIDECAR_PORT` | Loopback host port for the sidecar (default `8000`). | Sidecar-specific env (DB DSN, `USER_BASEPATHS`, etc.) is documented in [`sidecar/README.md`](sidecar/README.md). ## Read-only libraries The default originals mount is `:rw` because file operations (rename, folder mutations, duplicate archive, heap convert) need to mutate the filesystem. To run against a read-only archive, set `PP_ORIGINALS_MODE=ro` in `.env`. Browsing, marks, ratings, and color labels still work; the following sidecar endpoints return an OS error: - `POST /api/sidecar/files/:uid/rename` - `POST /api/sidecar/folders` / `:rel/rename` / `DELETE /:rel` - `POST /api/sidecar/albums/:uid/convert` - `POST /api/sidecar/duplicates/archive` PhotoPrism's `PHOTOPRISM_READONLY` is controlled separately by `PP_READONLY` and gates its own backwrite / import paths. ## Mobile & third-party apps (per-user) PhotoPrism CE does **not** enforce `auth_users.base_path` on API reads — any authenticated user can search the whole library. The sidecar therefore ships a scoping proxy at `/api/v1/*` (see [`sidecar/handlers_ppproxy.go`](sidecar/handlers_ppproxy.go)) and the reverse proxy routes the public `/api/v1` there instead of straight to PhotoPrism. Result: any PhotoPrism-compatible app pointed at the site sees only the logged-in user's photos. - **Server URL for apps**: the site itself (e.g. `https://photos.hubris.network`). Known-good client: [Gallery for PhotoPrism](https://github.com/Radiokot/photoprism-android-client) (Android/F-Droid). - **Login**: the user's normal username/password. For OIDC accounts (no password), mint an app password: `docker exec pp-app photoprism auth add -n "gallery" -s "*" ` and use it as the password in the app. - **What's scoped**: photo/geo searches, per-photo reads and edits, batch operations, downloads by UID. Hash-addressed media (thumbnails, video streams, file downloads) is token-guarded and passes through. - **What's shared** (CE has no per-user variants of these): album *names*, labels, and people — the photos inside them stay scoped. Album zip downloads are generated by PhotoPrism and are not scoped. - **Uploads**: the reconciler mirrors `base_path` into `upload_path`, so WebDAV/app uploads land inside the user's own subtree. - Sessions with the `admin` role bypass the proxy scoping entirely (the web client's settings/users/index dialogs need the raw API). ## Dev iteration loop For fast iteration on the sidecar without rebuilding its image on every change, run it as a host process — bring up just `mariadb` and `photoprism` from compose, then build and run the Go binary locally. Full instructions in [`sidecar/README.md`](sidecar/README.md#dev-iteration-loop-host-build). ## Layout ```text . ├── docker-compose.yml base stack: mariadb + photoprism + sidecar ├── docker-compose.podman.yml rootless-podman overlay (keep-id mapping) ├── docker-compose.gpu.yml opt-in VA-API GPU passthrough overlay ├── .env.example required env vars (copy to .env) ├── mariadb/init/ first-boot SQL: creates mule_sidecar DB + user ├── pp/ PhotoPrism bind-mounted state (storage, import) ├── sidecar/ Go service — see sidecar/README.md └── web/ SvelteKit frontend ``` ## GPU video acceleration (optional) Hosts with a VA-API-capable GPU (Intel iGPU, AMD APU, etc.) can layer [`docker-compose.gpu.yml`](docker-compose.gpu.yml) to hand `/dev/dri/*` to PhotoPrism and switch ffmpeg to hardware encode/decode — a large perf win for video thumbnails and HEVC→H.264 transcodes: ```bash docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d ``` Set `PP_FFMPEG_ENCODER=vaapi` in `.env` (default for the overlay). Verify with `docker exec pp-app photoprism show config | grep -i ffmpeg`. [pp]: https://photoprism.app/