# 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.

## 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/
