Default PHOTOPRISM_INDEX_WORKERS is NumCPU/2 (3 on the M0 6-core test LXC). Each worker forks TF + ffmpeg + libvips, so a fresh index of ~1.2k photos pushed the LXC's load avg above 50 and starved the Proxmox host. Pin to PP_WORKERS / PP_INDEX_WORKERS (default 2) so the indexer is calm by default; bump in .env.photoprism on dedicated boxes.
192 lines
8.8 KiB
YAML
192 lines
8.8 KiB
YAML
# PhotoPrism stack — replaces the legacy mule-image backend over the course of
|
||
# milestones M0–M5 (see /home/dtoro/.claude/plans/i-want-you-to-twinkly-galaxy.md).
|
||
#
|
||
# This compose file is intentionally separate from docker-compose.yml so the
|
||
# legacy mule-image stack keeps running side-by-side until M5 cutover, when
|
||
# data is migrated and the legacy backend is retired.
|
||
#
|
||
# docker compose -f docker-compose.photoprism.yml up -d
|
||
#
|
||
# M0 scope: mariadb + photoprism only. Library mounted READ-ONLY so initial
|
||
# indexing cannot mutate originals while we validate. Backwrite, sidecar
|
||
# service, web frontend, and reverse proxy land in later milestones.
|
||
|
||
services:
|
||
mariadb:
|
||
# Fully-qualified for podman (which refuses short names by default).
|
||
# Docker resolves the same digest.
|
||
image: docker.io/library/mariadb:11
|
||
container_name: pp-mariadb
|
||
restart: unless-stopped
|
||
command:
|
||
- --innodb-buffer-pool-size=512M
|
||
- --transaction-isolation=READ-COMMITTED
|
||
- --character-set-server=utf8mb4
|
||
- --collation-server=utf8mb4_unicode_ci
|
||
- --max-connections=512
|
||
- --innodb-rollback-on-timeout=OFF
|
||
- --innodb-lock-wait-timeout=120
|
||
environment:
|
||
MARIADB_AUTO_UPGRADE: "1"
|
||
MARIADB_INITDB_SKIP_TZINFO: "1"
|
||
MARIADB_DATABASE: ${PP_DB_NAME:-photoprism}
|
||
MARIADB_USER: ${PP_DB_USER:-photoprism}
|
||
MARIADB_PASSWORD: ${PP_DB_PASSWORD:?set PP_DB_PASSWORD in .env.photoprism}
|
||
MARIADB_ROOT_PASSWORD: ${PP_DB_ROOT_PASSWORD:?set PP_DB_ROOT_PASSWORD in .env.photoprism}
|
||
# Loopback-only host port so the mule-sidecar (running as a host process
|
||
# in M4) can reach `mule_sidecar.*` over TCP. Not exposed beyond
|
||
# 127.0.0.1; the photoprism container still resolves mariadb by service
|
||
# name on the photoprism-network bridge.
|
||
ports:
|
||
- "127.0.0.1:${PP_DB_PORT:-3306}:3306"
|
||
volumes:
|
||
- pp_mariadb_data:/var/lib/mysql
|
||
# The init script creates the mule_sidecar database + user that the Go
|
||
# sidecar service will use in M4. Idempotent; no-op on subsequent boots.
|
||
# ":Z" is the SELinux private-relabel flag — needed on Fedora/RHEL hosts,
|
||
# silently no-op on Debian/Ubuntu and macOS Docker Desktop.
|
||
- ./mariadb/init:/docker-entrypoint-initdb.d:ro,Z
|
||
healthcheck:
|
||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 12
|
||
start_period: 60s
|
||
networks: [photoprism-network]
|
||
|
||
photoprism:
|
||
image: docker.io/photoprism/photoprism:latest
|
||
container_name: pp-app
|
||
restart: unless-stopped
|
||
depends_on:
|
||
mariadb:
|
||
condition: service_healthy
|
||
# PhotoPrism's container drops to a non-root user via PHOTOPRISM_UID /
|
||
# PHOTOPRISM_GID. Match the host user that owns ${PHOTO_DIRS} so the
|
||
# process can read originals (and later write sidecars).
|
||
user: "${PP_UID:-1000}:${PP_GID:-1000}"
|
||
security_opt:
|
||
- seccomp:unconfined
|
||
- apparmor:unconfined
|
||
ports:
|
||
- "${PP_PORT:-2342}:2342"
|
||
environment:
|
||
PHOTOPRISM_ADMIN_USER: ${PP_ADMIN_USER:-admin}
|
||
PHOTOPRISM_ADMIN_PASSWORD: ${PP_ADMIN_PASSWORD:?set PP_ADMIN_PASSWORD in .env.photoprism}
|
||
PHOTOPRISM_AUTH_MODE: ${PP_AUTH_MODE:-password}
|
||
PHOTOPRISM_SITE_URL: ${PP_SITE_URL:-http://localhost:2342/}
|
||
PHOTOPRISM_ORIGINALS_LIMIT: ${PP_ORIGINALS_LIMIT:-50000}
|
||
PHOTOPRISM_HTTP_COMPRESSION: gzip
|
||
PHOTOPRISM_LOG_LEVEL: ${PP_LOG_LEVEL:-info}
|
||
# Indexer concurrency. Defaults to NumCPU/2 (= 3 on a 6-core LXC),
|
||
# but each worker forks TF + ffmpeg + libvips so effective load is
|
||
# much higher — a fresh index of 1.2k photos on M0 pushed the LXC
|
||
# load to 50+ and starved sibling containers. Pin to a low value
|
||
# for shared hosts; raise on dedicated machines.
|
||
PHOTOPRISM_WORKERS: ${PP_WORKERS:-2}
|
||
PHOTOPRISM_INDEX_WORKERS: ${PP_INDEX_WORKERS:-${PP_WORKERS:-2}}
|
||
# M0 safety: keep originals read-only. Flip to "false" in M2 when the
|
||
# right-sidebar enables metadata edits and we want EXIF backwrite.
|
||
PHOTOPRISM_READONLY: ${PP_READONLY:-true}
|
||
PHOTOPRISM_EXPERIMENTAL: "false"
|
||
PHOTOPRISM_DISABLE_CHOWN: "true"
|
||
PHOTOPRISM_DISABLE_WEBDAV: ${PP_DISABLE_WEBDAV:-false}
|
||
PHOTOPRISM_DISABLE_SETTINGS: "false"
|
||
PHOTOPRISM_DISABLE_TLS: "true"
|
||
PHOTOPRISM_DEFAULT_TLS: "false"
|
||
# AI/vision pipeline back on — per plan we re-introduce TF labels + faces.
|
||
PHOTOPRISM_TENSORFLOW_OFF: "false"
|
||
PHOTOPRISM_DETECT_NSFW: "true"
|
||
PHOTOPRISM_UPLOAD_NSFW: "true"
|
||
# Database
|
||
PHOTOPRISM_DATABASE_DRIVER: mysql
|
||
PHOTOPRISM_DATABASE_SERVER: mariadb:3306
|
||
PHOTOPRISM_DATABASE_NAME: ${PP_DB_NAME:-photoprism}
|
||
PHOTOPRISM_DATABASE_USER: ${PP_DB_USER:-photoprism}
|
||
PHOTOPRISM_DATABASE_PASSWORD: ${PP_DB_PASSWORD}
|
||
# Sidecars next to originals — read by the migrator at M5.
|
||
PHOTOPRISM_SIDECAR_PATH: ""
|
||
PHOTOPRISM_SIDECAR_YAML: "true"
|
||
# EXIF backwrite — disabled in M0 (READONLY blocks writes anyway).
|
||
# Re-enable in M2 by overriding in .env.photoprism: PP_BACKUP_DATABASE=true.
|
||
PHOTOPRISM_DISABLE_BACKUPS: "false"
|
||
PHOTOPRISM_BACKUP_DATABASE: ${PP_BACKUP_DATABASE:-true}
|
||
PHOTOPRISM_DISABLE_EXIFTOOL: "false"
|
||
# OIDC — set in .env.photoprism when the IdP (Authentik) is wired up.
|
||
# Empty values keep OIDC dormant; the username/password login still works.
|
||
# PhotoPrism's CLI flags are --oidc-uri / --oidc-client / --oidc-secret
|
||
# / --oidc-provider, so the env-var names it actually reads are
|
||
# PHOTOPRISM_OIDC_URI / _CLIENT / _SECRET / _PROVIDER (NOT _ISSUER_URL
|
||
# / _CLIENT_ID / _CLIENT_SECRET / _PROVIDER_NAME — those are silently
|
||
# ignored, OIDC stays dormant, and `photoprism show config` reports
|
||
# blank oidc-uri / oidc-client). PHOTOPRISM_OIDC_REDIRECT is a bool
|
||
# (auto-redirect-from-/library/login), not a URL — PhotoPrism builds
|
||
# the callback from PHOTOPRISM_SITE_URL.
|
||
PHOTOPRISM_OIDC_PROVIDER: ${OIDC_PROVIDER_NAME:-${OIDC_PROVIDER:-}}
|
||
PHOTOPRISM_OIDC_URI: ${OIDC_ISSUER_URL:-${OIDC_URI:-}}
|
||
PHOTOPRISM_OIDC_CLIENT: ${OIDC_CLIENT_ID:-${OIDC_CLIENT:-}}
|
||
PHOTOPRISM_OIDC_SECRET: ${OIDC_CLIENT_SECRET:-${OIDC_SECRET:-}}
|
||
PHOTOPRISM_OIDC_SCOPES: ${OIDC_SCOPES:-openid profile email}
|
||
PHOTOPRISM_OIDC_REGISTER: ${OIDC_REGISTER:-true}
|
||
PHOTOPRISM_OIDC_ROLE: ${OIDC_ROLE:-user}
|
||
PHOTOPRISM_OIDC_REDIRECT: ${OIDC_REDIRECT:-false}
|
||
working_dir: /photoprism
|
||
volumes:
|
||
# Existing photo library — mounted read-only in M0; flip to :rw in M2
|
||
# when the right-sidebar starts saving edits. ",Z" relabels for SELinux
|
||
# on Fedora/RHEL; silent no-op elsewhere.
|
||
- "${PHOTO_DIRS:?set PHOTO_DIRS in .env.photoprism}:/photoprism/originals:${PP_ORIGINALS_MODE:-ro},Z"
|
||
- "./pp/storage:/photoprism/storage:Z"
|
||
- "./pp/import:/photoprism/import:Z"
|
||
networks: [photoprism-network]
|
||
|
||
# mule-sidecar — Go + Gin + GORM service for endpoints PhotoPrism's API
|
||
# does not expose (file rename, folder mutations, heap convert, duplicate
|
||
# scan, per-photo marks). Same wire contract as the M3 Node prototype;
|
||
# the SvelteKit dev server proxies /api/sidecar/* here.
|
||
sidecar:
|
||
build:
|
||
context: ./sidecar
|
||
container_name: pp-sidecar
|
||
restart: unless-stopped
|
||
depends_on:
|
||
mariadb:
|
||
condition: service_healthy
|
||
photoprism:
|
||
condition: service_started
|
||
# Match PhotoPrism's UID/GID so renames/folder mutations preserve the
|
||
# ownership the indexer expects on the bind-mounted originals.
|
||
user: "${PP_UID:-1000}:${PP_GID:-1000}"
|
||
ports:
|
||
# Loopback only — Vite (host) proxies /api/sidecar/* to this port.
|
||
# Behind a reverse proxy in production; never published beyond the
|
||
# host.
|
||
- "127.0.0.1:${SIDECAR_PORT:-8000}:8000"
|
||
environment:
|
||
ORIGINALS_ROOT: /photoprism/originals
|
||
PHOTOPRISM_BASE_URL: http://photoprism:2342
|
||
# Bind on all interfaces inside the container so the host-side
|
||
# 127.0.0.1:8000 port mapping can reach the listener. The Go
|
||
# binary defaults to 127.0.0.1 for the host-mode dev loop.
|
||
SIDECAR_LISTEN_ADDR: 0.0.0.0
|
||
SIDECAR_PORT: "8000"
|
||
SIDECAR_DB_HOST: mariadb
|
||
SIDECAR_DB_PORT: "3306"
|
||
SIDECAR_DB_USER: sidecar
|
||
# Rotate before any non-local deployment. Provisioned by
|
||
# mariadb/init/01-sidecar.sql on first boot of the mariadb volume.
|
||
SIDECAR_DB_PASSWORD: ${SIDECAR_DB_PASSWORD:-replace-at-m4-bringup}
|
||
SIDECAR_DB_NAME: mule_sidecar
|
||
volumes:
|
||
# Sidecar mutates originals (rename, folder mutations, heap
|
||
# convert) — always rw regardless of PhotoPrism's mount mode.
|
||
- "${PHOTO_DIRS:?set PHOTO_DIRS in .env.photoprism}:/photoprism/originals:rw,Z"
|
||
networks: [photoprism-network]
|
||
|
||
networks:
|
||
photoprism-network:
|
||
driver: bridge
|
||
|
||
volumes:
|
||
pp_mariadb_data:
|