Installation¶
Proton Faces ships as a Docker Compose stack of three services: proton-bridge, indexer, and app. You can also run it as a single process for local development or run the demo without any Proton account.
Pick a mode¶
๐ณ Docker Compose (recommended)¶
Three containers, isolated processes, ML on dedicated cores, single docker compose up to start.
๐ฌ Demo mode¶
No Proton account, no session file, no configuration. Try every feature on a fixture of CC0 photos.
๐ ๏ธ Single process¶
Set RUN_INDEXER=1 on the app container to run the recognition pipeline in-process. Handy for debugging or low-resource boxes.
๐งโ๐ป Local dev (Python)¶
Install requirements, run python main.py. See reference/configuration.md for the env vars you need.
Docker Compose (production layout)¶
1. Get a Proton session file¶
First-time user? Install the Proton Drive CLI, run proton-drive auth login once (browser-based, ~2 minutes), then export the session:
Already have the CLI session in pass? The one-liner works too:
See the Session file guide for the full recipe, including the macOS Keychain variant, the Windows Credential Manager, and the most secure option โ the SDK's GPG-encrypted pass store (PROTON_DRIVE_CREDENTIALS_STORE=pass).
Keep this file private
auth-session.json contains your account tokens. It is mounted into the bridge container only (issue #32) โ the indexer and the internet-facing app container have no path to it, so a vulnerability in one of them cannot leak your session. The mount is writable because the SDK rewrites the file on token refresh; see Session file for the full layout. The file should never be committed to git (the repo's .gitignore already excludes credentials/).
2. Configure¶
cp .env.example .env
# Inside the containers DATA_DIR is always /data.
# To persist data on a specific host disk, set the compose-level mount:
# DATA_MOUNT=/srv/proton-faces/data
3. Start¶
Then open http://localhost:8080 and sign in with the admin account you created with:
Prebuilt images are published to the GitHub Container Registry, so docker compose up pulls them โ no building on the server. To build from source instead, use docker compose build --pull (or --build).
4. Watch the indexer catch up¶
You'll see lines like:
INFO indexer: sync: 1234 remote, 87 new, 0 gone
INFO indexer: processed pic-...: 2 faces, clip=ok
INFO indexer: cluster: 5 clusters from 23 faces
The web UI becomes useful right away as results stream in.
Single-process layout (fallback)¶
This collapses the indexer into the app container. Same UI, same API. Slightly worse latency on small boxes (face detection preempts the API event loop), but fewer moving parts.
Local Python install¶
For hacking on the code itself:
cd app
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# Bake or download the ML models into ./data/models (see Dockerfile for the steps)
DATA_DIR=./data MODELS_DIR=./data/models python src/main.py
Open http://localhost:8080.
Volumes¶
| Path | Purpose | Persist by default? |
|---|---|---|
data (named volume) |
Thumbnails + SQLite + vector index | Yes |
credentials/ |
Read-only mount of your Proton session | You manage |
To persist the data volume on a specific host disk, set DATA_MOUNT in .env:
The directory must be writable by UID 1000 (the user all containers run as).
Running the image directly (
docker run). The images do not declare aVOLUME, so when you run them outside compose you must mount the data directory yourself, e.g.-v proton-faces-data:/data. Compose does this automatically (it mounts${DATA_MOUNT:-data}:/dataon every service); the note only matters for manualdocker runexperiments, where an unmounted/datawrites to the container's ephemeral filesystem and is lost when the container is removed.
Updating¶
The data volume is preserved across updates. The schema migrates automatically on init_db().
Pinning images to a version or digest¶
compose.yml references ghcr.io/mmornati/proton-faces-*:latest by default, so docker compose pull always fetches the newest published image. For a supply-chain-conscious deployment you can pin to an immutable reference instead:
- Versioned tag โ every
v*git tag publishesghcr.io/mmornati/proton-faces-{bridge,app}:<tag>(in addition to:latest). Pin a release:
- Digest pin โ the strongest guarantee: the image is identified by its content hash, so a compromised or broken publish can never be pulled silently. Resolve the digest of a known-good tag, then pin it:
image: ghcr.io/mmornati/proton-faces-app@sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
With a digest pin, docker compose pull only updates when the digest itself changes โ bump it deliberately when you want to upgrade.
Releases¶
Releases are cut from git tags. Pushing a v* tag does two things: the publish workflow builds and pushes the versioned images to GHCR, and the release workflow creates a GitHub Release whose notes list every merged PR since the previous tag (with links) plus a full changelog diff.
To cut a release:
Or run the Release workflow from the Actions tab and enter the version (e.g. 1.2.3) โ it creates and pushes the tag for you.
Each release links the matching images, e.g. ghcr.io/mmornati/proton-faces-app:v1.2.3. The images are the release artifacts โ there are no binary assets attached to the release itself. Deploy a specific release by pinning compose.yml as shown above, or track the latest with docker compose pull && docker compose up -d.
Uninstalling¶
docker compose down # stop containers, keep the data volume
docker compose down --volumes # also wipe the data volume
The bridge container is stateless (the session file lives on the data volume too, via PROTON_DRIVE_CACHE_DIR); wiping it logs the bridge out.
Next: the Quickstart walks you through your first search.