# 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} # 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_OIDC_PROVIDER_NAME: ${OIDC_PROVIDER_NAME:-} PHOTOPRISM_OIDC_ISSUER_URL: ${OIDC_ISSUER_URL:-} PHOTOPRISM_OIDC_CLIENT_ID: ${OIDC_CLIENT_ID:-} PHOTOPRISM_OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-} PHOTOPRISM_OIDC_REDIRECT_URI: ${OIDC_REDIRECT_URI:-} PHOTOPRISM_OIDC_SCOPES: ${OIDC_SCOPES:-openid profile email} PHOTOPRISM_OIDC_REGISTER: ${OIDC_REGISTER:-true} PHOTOPRISM_OIDC_ROLE: ${OIDC_ROLE:-user} 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: