Videos, Live Photos, and RAW+JPG pairs keep several files under Root "/". The old movePhotoFiles moved only the primary (often the poster JPG), orphaning the .mov: PhotoPrism then saw the photo as moved (dropped from the grid) while the video stayed behind and broke. Move the whole originals group under one shared stem (new uniqueStem helper) so siblings re-stack after reindex; fail the photo and report it if any sibling can't move. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mule-image
Self-hosted photo management built on top of PhotoPrism. A SvelteKit
frontend (web/) plus a small Go service (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
┌──────────────────┐ /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):
- 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
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:
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. 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. |
SIDECAR_PORT |
Loopback host port for the sidecar (default 8000). |
Sidecar-specific env (DB DSN, USER_BASEPATHS, etc.) is documented in
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/renamePOST /api/sidecar/folders/:rel/rename/DELETE /:relPOST /api/sidecar/albums/:uid/convertPOST /api/sidecar/duplicates/archive
PhotoPrism's PHOTOPRISM_READONLY is controlled separately by
PP_READONLY and gates its own backwrite / import paths.
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.
Layout
.
├── 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 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:
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.