Merge remote-tracking branch 'origin/develop' into PostgreSQL

This commit is contained in:
Keith Martin 2026-05-23 21:35:26 +10:00
commit 40ce242e89
90 changed files with 1749 additions and 208 deletions

View file

@ -14,6 +14,7 @@
/assets/models/
/storage/
/build/
/setup/Makefile
/photoprism
/photoprism-*
/frontend/tests/acceptance/screenshots/
@ -69,6 +70,7 @@ venv
.nv
.eslintcache
.gocache
.gitconfig
.DS_Store
.DS_Store?
._*

View file

@ -16,6 +16,12 @@
# read by Docker Compose itself (so no --profile flag is required).
# COMPOSE_PROFILES=postgres
# In-container working directory and bind mount target for the repo checkout.
# Override when the host clone lives outside the default GOPATH layout; the
# value is reused for the Dockerfile WORKDIR (build arg), working_dir, the
# source bind mount, and every PhotoPrism path env var so they stay in sync.
# WORKING_DIR=/go/src/github.com/photoprism/photoprism
# Network interfaces to which Traefik and other services bind on the host.
# Defaults expose Traefik on every interface (so *.localssl.dev is reachable
# from the LAN) and keep direct service ports on loopback only.

View file

@ -12,18 +12,10 @@ body:
2. Work through the [Troubleshooting Checklists](https://docs.photoprism.app/getting-started/troubleshooting/) we provide.
3. Do not report [known issues](https://docs.photoprism.app/known-issues/) or [missing features](https://github.com/photoprism/photoprism/issues) as bugs.
4. Use [GitHub Discussions](https://github.com/photoprism/photoprism/discussions) for community help, and visit our [Support Guide](https://www.photoprism.app/kb/getting-support) to learn more about support options.
- type: checkboxes
id: prerequisites
attributes:
label: Before You Continue
description: Confirm that you reviewed the guidance above.
options:
- label: This is a new, confirmed bug that has not yet been reported or documented
required: true
- type: textarea
id: documented-behavior
attributes:
label: What Is Not Working as Documented?
label: What is not working as documented?
description: Be specific and include links to the relevant documentation when possible.
placeholder: Describe the incorrect behavior and link to the related documentation on https://docs.photoprism.app/.
validations:
@ -31,62 +23,65 @@ body:
- type: textarea
id: reproduction-steps
attributes:
label: How Can We Reproduce It?
label: How can we reproduce it?
description: Provide numbered steps so we can reproduce the behavior reliably.
placeholder: |
1. Go to ...
2. Click ...
3. Scroll to ...
4. Expected vs. actual result
render: markdown
validations:
required: true
- type: checkboxes
id: prerequisites
attributes:
label: Have you verified that no similar reports exist?
description: Confirm that you read the relevant documentation, this is not a support request, and that no similar reports already exist.
options:
- label: This is a new bug that has not yet been reported or documented
required: true
- type: textarea
id: expected-behavior
attributes:
label: What Behavior Do You Expect?
label: What behavior do you expect?
description: Share the correct outcome you expected to see.
validations:
required: true
- type: textarea
id: possible-cause
attributes:
label: What Could Be the Cause?
label: What could be the cause?
description: Summarize any investigation you have already completed and the potential root cause.
- type: textarea
id: supporting-material
attributes:
label: Logs, Sample Files, or Screenshots
description: Attach or link to files that help us reproduce the problem. Import and indexing issues require sample files and logs.
placeholder: Provide links to archives, logs, or screenshots. See https://www.photoprism.app/contact for secure sharing options.
- type: textarea
id: software-versions
attributes:
label: Which Software Versions Do You Use?
label: Which software versions do you use?
description: Include all relevant software versions so we can reproduce the environment.
placeholder: |
- PhotoPrism Architecture & Build Number (AMD64, ARM64, ARMv7, ...):
- Database Type & Version (MariaDB, MySQL, SQLite, ...):
- Operating System Types & Versions (Linux, Windows, Android, ...):
- Browser Types & Versions (Firefox, Chrome, Safari on iPhone, ...):
- Ad Blockers, Browser Plugins, or Firewall Software:
render: markdown
value: |
- PhotoPrism Edition & Version (Build):
- Database Type & Version(s):
- Operating System(s):
- Browser Type & Version(s):
- Ad Blockers, Antivirus, and Plugins:
validations:
required: true
- type: textarea
id: installation-device
attributes:
label: On What Device Is PhotoPrism Installed?
label: On what device is PhotoPrism installed?
description: Provide hardware details, especially for performance, import, or indexing issues.
placeholder: |
- Device / Processor Type (Raspberry Pi 4, Intel Core i7-3770, AMD Ryzen 7 3800X, ...):
- Physical Memory & Swap Space (GB):
- Storage Type (HDD, SSD, RAID, USB, Network Storage, ...):
- Additional Context:
render: markdown
value: |
- CPU/Device Type(s):
- Physical Memory & Swap in GB:
- Storage (HDD/SSD/RAID, USB, Network):
- type: textarea
id: networking-setup
attributes:
label: Do You Use a Reverse Proxy, Firewall, VPN, or CDN?
label: Do you use a reverse proxy, firewall, VPN, or CDN?
description: Describe your network setup. If applicable, include details about your NGINX or other reverse proxy configuration.
placeholder: Describe proxies, VPNs, CDNs, or firewall software involved. Include versions and configuration snippets when helpful.
- type: upload
id: supporting-material
attributes:
label: Logs, Sample Files, or Screenshots
description: Attach or link to files that help us reproduce the problem. Import and indexing issues require sample files and logs.

View file

@ -10,21 +10,15 @@ body:
Before submitting a new request, please check our GitHub roadmap and issues for similar requests:
- https://link.photoprism.app/roadmap
- https://github.com/photoprism/photoprism/issues
- type: checkboxes
id: request-prep
attributes:
label: Before You Continue
description: Make sure you completed these steps before opening a new request.
options:
- label: This is not a support request, and I verified that no similar issues exist
required: true
- type: textarea
id: user-story
attributes:
label: User Story
description: |
Feature requests MUST begin with a one sentence user story in the format `**As a <role>, I want <goal>, so that <outcome>.**`
Feature requests MUST begin with a one sentence user story.
**As a <role>, I want <goal>, so that <outcome>.**
placeholder: "**As a <role>, I want <goal>, so that <outcome>.**"
value: "**As a <role>, I want <goal>, so that <outcome>.**"
validations:
required: true
- type: textarea
@ -49,36 +43,31 @@ body:
label: Which alternatives or workarounds have you considered?
description: Describe alternative solutions, workarounds, or related features you evaluated.
placeholder: Mention any alternative solutions, workarounds, or related features you evaluated.
- type: checkboxes
id: prerequisites
attributes:
label: Have you verified that no similar issues exist?
options:
- label: This is not a support request, and I verified that no similar issues exist
required: true
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any other context or screenshots about the feature request here.
placeholder: Add any other context or screenshots that help explain the request.
- type: textarea
id: open-questions
attributes:
label: Open Questions
description: Use checklist items to capture ambiguities, tradeoffs, or decisions that still need clarification.
placeholder: |
- [ ] <question> — options A/B/C and implications.
- [ ] <question> — potential ambiguity or conflict to clarify.
render: markdown
- type: textarea
id: acceptance-criteria
attributes:
label: Acceptance Criteria
description: Use checklist items and RFC 2119 keywords such as MUST, SHOULD, and MAY to define the expected behavior.
placeholder: |
- [ ] <component> MUST <expected behavior>
- [ ] <component> SHOULD <expected behavior>
- [ ] <component> MAY <expected behavior>
render: markdown
- [ ] <component> MUST <expected behavior>.
- [ ] <component> SHOULD <expected behavior>.
- [ ] <component> MAY <expected behavior>.
value: |
- [ ] <component> MUST <expected behavior>.
- [ ] <component> SHOULD <expected behavior>.
- [ ] <component> MAY <expected behavior>.
validations:
required: true
- type: textarea
id: references
attributes:
label: References
description: Add links to external documentation, references, or examples.
placeholder: Add links to documentation, references, or examples that support the request.

2
.gitignore vendored
View file

@ -33,6 +33,7 @@ venv
.nv
.eslintcache
.gocache
.gitconfig
.system
/pro/
/portal/
@ -58,6 +59,7 @@ frontend/coverage/
/photos/import/*
/storage/
/build/
/setup/Makefile
/assets/docs/
/assets/facenet/
/assets/nasnet/

3
.rsyncignore Normal file
View file

@ -0,0 +1,3 @@
.*
*.db
__*

View file

@ -18,16 +18,19 @@ ENV NPM_CONFIG_IGNORE_SCRIPTS=true
# FROM photoprism/develop:bullseye # Debian 11 (Bullseye)
# FROM photoprism/develop:buster # Debian 10 (Buster)
# Set default working directory.
WORKDIR "/go/src/github.com/photoprism/photoprism"
# Set default working directory. Override WORKING_DIR (e.g. via compose build
# args populated from .env) to match a custom host-side clone layout; the value
# must stay aligned with compose's working_dir and the source bind mount target.
ARG WORKING_DIR=/go/src/github.com/photoprism/photoprism
WORKDIR "${WORKING_DIR}"
# Copy source to image.
COPY . .
COPY --chown=root:root /scripts/dist/ /scripts/
COPY --chown=root:root ./scripts/dist/ /scripts/
# Re-install the dev "mariadb" client config so a custom MARIADB_PORT in .env
# is honored even when the base image was built before the port=<n> line was
# removed (no-op once the next dated base image picks up the new .my.cnf).
COPY --chown=root:root --chmod=644 .my.cnf /etc/my.cnf
COPY --chown=root:root --chmod=644 ./.my.cnf /etc/my.cnf
RUN sudo /scripts/install-yt-dlp.sh

View file

@ -1,4 +1,4 @@
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/
@ -39,6 +39,7 @@ BUILD_ARCH ?= $(shell ./scripts/dist/arch.sh)
export BUILD_ARCH
JS_BUILD_PATH ?= $(shell realpath "./assets/static/build")
TF_VERSION ?= 2.18.0
WORKING_DIR ?= /go/src/github.com/photoprism/photoprism
# Install parameters.
INSTALL_PATH ?= $(BUILD_PATH)/photoprism-ce_$(BUILD_TAG)-$(shell echo $(BUILD_OS) | tr '[:upper:]' '[:lower:]')-$(BUILD_ARCH)
@ -186,10 +187,10 @@ notice:
fix-permissions:
$(info Updating filesystem permissions...)
@if [ $(UID) != 0 ]; then\
echo "Running \"chown --preserve-root -Rcf $(UID):$(GID) /go /photoprism /opt/photoprism /tmp/photoprism\". Please wait."; \
sudo chown --preserve-root -Rcf $(UID):$(GID) /go /photoprism /opt/photoprism /tmp/photoprism || true;\
echo "Running \"chmod --preserve-root -Rcf u+rwX /go/src/github.com/photoprism/* /photoprism /opt/photoprism /tmp/photoprism\". Please wait.";\
sudo chmod --preserve-root -Rcf u+rwX /go/src/github.com/photoprism/photoprism/* /photoprism /opt/photoprism /tmp/photoprism || true;\
echo "Running \"chown --preserve-root -Rcf $(UID):$(GID) /go $(WORKING_DIR) /photoprism /opt/photoprism /tmp/photoprism\". Please wait."; \
sudo chown --preserve-root -Rcf $(UID):$(GID) /go $(WORKING_DIR) /photoprism /opt/photoprism /tmp/photoprism || true;\
echo "Running \"chmod --preserve-root -Rcf u+rwX $(WORKING_DIR)/* /photoprism /opt/photoprism /tmp/photoprism\". Please wait.";\
sudo chmod --preserve-root -Rcf u+rwX $(WORKING_DIR)/* /photoprism /opt/photoprism /tmp/photoprism || true;\
echo "Done."; \
else\
echo "Running as root. Nothing to do."; \

View file

@ -1,5 +1,6 @@
examples
README.md
Thumbs.db
docs
.*
_*
_*

View file

@ -46,10 +46,11 @@ services:
PHOTOPRISM_TEST_DSN_SQLITE: ""
PHOTOPRISM_TEST_DSN_SQLITEFILE: ".test.db?_foreign_keys=on&_busy_timeout=5000"
PHOTOPRISM_TEST_DSN_POSTGRES: "postgresql://testdb:testdb@postgres:${POSTGRES_PORT:-4002}/testdb?TimeZone=UTC&connect_timeout=15&lock_timeout=5000&sslmode=disable"
PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets"
PHOTOPRISM_STORAGE_PATH: "/go/src/github.com/photoprism/photoprism/storage"
PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals"
PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/storage/import"
WORKING_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}"
PHOTOPRISM_ASSETS_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/assets"
PHOTOPRISM_STORAGE_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage"
PHOTOPRISM_ORIGINALS_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage/originals"
PHOTOPRISM_IMPORT_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage/import"
PHOTOPRISM_DISABLE_CHOWN: "false" # Disables storage permission updates on startup
PHOTOPRISM_DISABLE_BACKUPS: "false" # Don't backup photo and album metadata to YAML files
PHOTOPRISM_DISABLE_WEBDAV: "false" # Disables built-in WebDAV server
@ -82,9 +83,9 @@ services:
# - "/dev/video11:/dev/video11" # Video4Linux (h264_v4l2m2m)
# - "/dev/dri/renderD128:/dev/dri/renderD128" # Intel GPU
# - "/dev/dri/card0:/dev/dri/card0"
working_dir: "/go/src/github.com/photoprism/photoprism"
working_dir: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}"
volumes:
- ".:/go/src/github.com/photoprism/photoprism"
- ".:${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}"
- "go-mod:/go/pkg/mod"
## MariaDB Database Server

View file

@ -1,7 +1,10 @@
services:
## PhotoPrism (Development Environment for Intel QSV hardware transcoding)
photoprism:
build: .
build:
context: .
args:
WORKING_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}"
image: photoprism/photoprism:develop
depends_on:
- mariadb
@ -107,11 +110,12 @@ services:
PHOTOPRISM_TEST_DSN_SQLITE: ""
PHOTOPRISM_TEST_DSN_SQLITEFILE: "file:/go/src/github.com/photoprism/photoprism/storage/testdata/unit.test.db?_foreign_keys=on&_busy_timeout=5000"
PHOTOPRISM_TEST_DSN_POSTGRES: "postgresql://testdb:testdb@postgres:${POSTGRES_PORT:-4002}/testdb?TimeZone=UTC&connect_timeout=15&lock_timeout=5000&sslmode=disable"
PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets"
PHOTOPRISM_STORAGE_PATH: "/go/src/github.com/photoprism/photoprism/storage"
PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals"
WORKING_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}"
PHOTOPRISM_ASSETS_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/assets"
PHOTOPRISM_STORAGE_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage"
PHOTOPRISM_ORIGINALS_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage/originals"
PHOTOPRISM_ORIGINALS_LIMIT: 128000 # sets originals file size limit to 128 GB
PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/storage/import"
PHOTOPRISM_IMPORT_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage/import"
PHOTOPRISM_DISABLE_CHOWN: "false" # disables updating storage permissions via chmod and chown on startup
PHOTOPRISM_DISABLE_BACKUPS: "false" # disables backing up albums and photo metadata to YAML files
PHOTOPRISM_DISABLE_WEBDAV: "false" # disables built-in WebDAV server
@ -136,17 +140,17 @@ services:
PHOTOPRISM_INIT: "https intel tensorflow"
## Additional dependencies and tools:
TF_CPP_MIN_LOG_LEVEL: 1
GOCACHE: "/go/src/github.com/photoprism/photoprism/.local/gocache"
CODEX_HOME: "/go/src/github.com/photoprism/photoprism/.local/codex"
CLAUDE_CONFIG_DIR: "/go/src/github.com/photoprism/photoprism/.local/claude"
GOCACHE: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/gocache"
CODEX_HOME: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/codex"
CLAUDE_CONFIG_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/claude"
GH_REPO: "photoprism/photoprism"
GH_CONFIG_DIR: "/go/src/github.com/photoprism/photoprism/.local/gh"
GH_CONFIG_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/gh"
## Share hardware devices with FFmpeg for hardware video transcoding:
devices:
- "/dev/dri:/dev/dri"
working_dir: "/go/src/github.com/photoprism/photoprism"
working_dir: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}"
volumes:
- ".:/go/src/github.com/photoprism/photoprism"
- ".:${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}"
- "./storage:/photoprism"
- "go-mod:/go/pkg/mod"

View file

@ -1,7 +1,10 @@
services:
## PhotoPrism (Development Environment for Nvidia)
photoprism:
build: .
build:
context: .
args:
WORKING_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}"
image: photoprism/photoprism:develop
runtime: nvidia
depends_on:
@ -110,11 +113,12 @@ services:
PHOTOPRISM_TEST_DSN_SQLITE: ""
PHOTOPRISM_TEST_DSN_SQLITEFILE: "file:/go/src/github.com/photoprism/photoprism/storage/testdata/unit.test.db?_foreign_keys=on&_busy_timeout=5000"
PHOTOPRISM_TEST_DSN_POSTGRES: "postgresql://testdb:testdb@postgres:${POSTGRES_PORT:-4002}/testdb?TimeZone=UTC&connect_timeout=15&lock_timeout=5000&sslmode=disable"
PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets"
PHOTOPRISM_STORAGE_PATH: "/go/src/github.com/photoprism/photoprism/storage"
PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals"
WORKING_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}"
PHOTOPRISM_ASSETS_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/assets"
PHOTOPRISM_STORAGE_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage"
PHOTOPRISM_ORIGINALS_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage/originals"
PHOTOPRISM_ORIGINALS_LIMIT: 128000 # sets originals file size limit to 128 GB
PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/storage/import"
PHOTOPRISM_IMPORT_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage/import"
PHOTOPRISM_DISABLE_CHOWN: "false" # disables updating storage permissions via chmod and chown on startup
PHOTOPRISM_DISABLE_BACKUPS: "false" # disables backing up albums and photo metadata to YAML files
PHOTOPRISM_DISABLE_WEBDAV: "false" # disables built-in WebDAV server
@ -145,18 +149,18 @@ services:
PHOTOPRISM_FFMPEG_BITRATE: "64" # video bitrate limit in Mbps (default: 60)
## Additional dependencies and tools:
TF_CPP_MIN_LOG_LEVEL: 1
GOCACHE: "/go/src/github.com/photoprism/photoprism/.local/gocache"
CODEX_HOME: "/go/src/github.com/photoprism/photoprism/.local/codex"
CLAUDE_CONFIG_DIR: "/go/src/github.com/photoprism/photoprism/.local/claude"
GOCACHE: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/gocache"
CODEX_HOME: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/codex"
CLAUDE_CONFIG_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/claude"
GH_REPO: "photoprism/photoprism"
GH_CONFIG_DIR: "/go/src/github.com/photoprism/photoprism/.local/gh"
GH_CONFIG_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/gh"
## Shared devices for video hardware transcoding (optional):
# devices:
# - "/dev/dri:/dev/dri" # Required Intel QSV or VAAPI hardware transcoding
# - "/dev/video11:/dev/video11" # Video4Linux Video Encode Device (h264_v4l2m2m)
working_dir: "/go/src/github.com/photoprism/photoprism"
working_dir: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}"
volumes:
- ".:/go/src/github.com/photoprism/photoprism"
- ".:${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}"
- "./storage:/photoprism"
- "go-mod:/go/pkg/mod"
## NVIDIA GPU Hardware Acceleration (see https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html):
@ -202,12 +206,26 @@ services:
OLLAMA_MULTIUSER_CACHE: "false" # optimize prompt caching for multi-user scenarios
OLLAMA_NOPRUNE: "false" # disables pruning of model blobs at startup
OLLAMA_NOHISTORY: "true" # disables readline history
OLLAMA_FLASH_ATTENTION: "false" # enables the experimental flash attention feature
OLLAMA_KV_CACHE_TYPE: "f16" # cache quantization (f16, q8_0, or q4_0)
# OLLAMA_FLASH_ATTENTION — small speedup on supported architectures
# (gemma3, gptoss, mistral3, qwen3*). Silently no-op on unsupported
# architectures and on CPU. Required to enable OLLAMA_KV_CACHE_TYPE
# quantization. Set to "false" for Qwen3-2507 builds (gh#12432).
OLLAMA_FLASH_ATTENTION: "true"
# OLLAMA_KV_CACHE_TYPE — cache precision (f16 is the default):
# f16 — native precision, works on every architecture
# q8_0 — halves cache VRAM; clean for qwen3*/gpt-oss/mistral3;
# 5x slowdown on gemma3 (gh#9683/#11949); no-op for
# gemma4/qwen2.5vl (not on FA allowlist — gh#13337)
# q4_0 — quarters cache VRAM; usable on qwen, hurts gemma;
# only reach for it when VRAM-constrained
OLLAMA_KV_CACHE_TYPE: "f16" # cache precision: f16 (default), q8_0, q4_0
OLLAMA_SCHED_SPREAD: "false" # allows scheduling models across all GPUs.
OLLAMA_NEW_ENGINE: "true" # enables the new Ollama engine
# OLLAMA_DEBUG: "true" # shows additional debug information
# OLLAMA_INTEL_GPU: "true" # enables experimental Intel GPU detection
## Telemetry / privacy opt-outs (containers do not inherit /etc/environment):
DO_NOT_TRACK: "true"
HF_HUB_DISABLE_TELEMETRY: "1"
# OLLAMA_NO_CLOUD: "1" # uncomment to disable Ollama Cloud models/features
## NVIDIA GPU Hardware Acceleration (see https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html):
NVIDIA_VISIBLE_DEVICES: "all"
NVIDIA_DRIVER_CAPABILITIES: "compute,utility"

View file

@ -4,7 +4,10 @@
services:
## PhotoPrism Build & Test Environment
photoprism:
build: .
build:
context: .
args:
WORKING_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}"
image: photoprism/photoprism:develop
depends_on:
- postgres
@ -115,11 +118,12 @@ services:
PHOTOPRISM_TEST_DSN_SQLITE: ""
PHOTOPRISM_TEST_DSN_SQLITEFILE: "file:/go/src/github.com/photoprism/photoprism/storage/testdata/unit.test.db?_foreign_keys=on&_busy_timeout=5000"
PHOTOPRISM_TEST_DSN_POSTGRES: "postgresql://testdb:testdb@postgres:${POSTGRES_PORT:-4002}/testdb?TimeZone=UTC&connect_timeout=15&lock_timeout=5000&sslmode=disable"
PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets"
PHOTOPRISM_STORAGE_PATH: "/go/src/github.com/photoprism/photoprism/storage"
PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals"
WORKING_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}"
PHOTOPRISM_ASSETS_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/assets"
PHOTOPRISM_STORAGE_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage"
PHOTOPRISM_ORIGINALS_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage/originals"
PHOTOPRISM_ORIGINALS_LIMIT: 128000 # sets originals file size limit to 128 GB
PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/storage/import"
PHOTOPRISM_IMPORT_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage/import"
PHOTOPRISM_DISABLE_CHOWN: "false" # disables updating storage permissions via chmod and chown on startup
PHOTOPRISM_DISABLE_BACKUPS: "false" # disables backing up albums and photo metadata to YAML files
PHOTOPRISM_DISABLE_WEBDAV: "false" # disables built-in WebDAV server
@ -151,18 +155,18 @@ services:
OLLAMA_API_KEY: "" # API key required to access Ollama (optional)
## Additional dependencies and tools:
TF_CPP_MIN_LOG_LEVEL: 1
GOCACHE: "/go/src/github.com/photoprism/photoprism/.local/gocache"
CODEX_HOME: "/go/src/github.com/photoprism/photoprism/.local/codex"
CLAUDE_CONFIG_DIR: "/go/src/github.com/photoprism/photoprism/.local/claude"
GOCACHE: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/gocache"
CODEX_HOME: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/codex"
CLAUDE_CONFIG_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/claude"
GH_REPO: "photoprism/photoprism"
GH_CONFIG_DIR: "/go/src/github.com/photoprism/photoprism/.local/gh"
GH_CONFIG_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/gh"
## Shared devices for video hardware transcoding (optional):
# devices:
# - "/dev/dri:/dev/dri" # Required Intel QSV or VAAPI hardware transcoding
# - "/dev/video11:/dev/video11" # Video4Linux Video Encode Device (h264_v4l2m2m)
working_dir: "/go/src/github.com/photoprism/photoprism"
working_dir: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}"
volumes:
- ".:/go/src/github.com/photoprism/photoprism"
- ".:${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}"
- "./storage:/photoprism"
- "go-mod:/go/pkg/mod"
@ -309,12 +313,15 @@ services:
OLLAMA_MULTIUSER_CACHE: "false" # optimize prompt caching for multi-user scenarios
OLLAMA_NOPRUNE: "false" # disables pruning of model blobs at startup
OLLAMA_NOHISTORY: "true" # disables readline history
OLLAMA_FLASH_ATTENTION: "false" # enables the experimental flash attention feature
OLLAMA_KV_CACHE_TYPE: "f16" # cache quantization (f16, q8_0, or q4_0)
OLLAMA_FLASH_ATTENTION: "true" # required for OLLAMA_KV_CACHE_TYPE quantization
OLLAMA_KV_CACHE_TYPE: "f16" # cache precision: f16 (default), q8_0, q4_0
OLLAMA_SCHED_SPREAD: "false" # allows scheduling models across all GPUs.
OLLAMA_NEW_ENGINE: "true" # enables the new Ollama engine
# OLLAMA_DEBUG: "true" # shows additional debug information
# OLLAMA_INTEL_GPU: "true" # enables experimental Intel GPU detection
## Telemetry / privacy opt-outs (containers do not inherit /etc/environment):
DO_NOT_TRACK: "true"
HF_HUB_DISABLE_TELEMETRY: "1"
# OLLAMA_NO_CLOUD: "1" # uncomment to disable Ollama Cloud models/features
## NVIDIA GPU Hardware Acceleration (optional):
# NVIDIA_VISIBLE_DEVICES: "all"
# NVIDIA_DRIVER_CAPABILITIES: "compute,utility"

View file

@ -1,7 +1,7 @@
#### Base Image: Ubuntu 24.04 LTS (Noble Numbat)
FROM ubuntu:noble
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Debian 12 (Bookworm)
FROM debian:bookworm-slim
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Debian 12 (Bookworm)
FROM debian:bookworm-slim
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Debian 11 (Bullseye)
FROM debian:bullseye-slim
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Debian 11 (Bullseye)
FROM golang:1-bullseye
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Debian 10 (Buster)
FROM golang:1-buster
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Ubuntu 21.10 (Impish Indri)
FROM ubuntu:impish
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Ubuntu 22.04 LTS (Jammy Jellyfish)
FROM ubuntu:jammy
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Ubuntu 22.04 LTS (Jammy Jellyfish)
FROM ubuntu:jammy
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Ubuntu 23.04 (Lunar Lobster)
FROM ubuntu:lunar
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Ubuntu 23.04 (Lunar Lobster)
FROM ubuntu:lunar
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Ubuntu 23.10 (Mantic Minotaur)
FROM ubuntu:mantic
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Ubuntu 23.10 (Mantic Minotaur)
FROM ubuntu:mantic
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Ubuntu 24.04 LTS (Noble Numbat)
FROM ubuntu:noble
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Ubuntu 24.04 LTS (Noble Numbat)
FROM ubuntu:noble
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Ubuntu 24.10 (Oracular Oriole)
FROM ubuntu:oracular
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Ubuntu 24.10 (Oracular Oriole)
FROM ubuntu:oracular
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Ubuntu 25.04 (Plucky Puffin)
FROM ubuntu:plucky
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Ubuntu 25.04 (Plucky Puffin)
FROM ubuntu:plucky
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Ubuntu 25.10 (Questing Quokka)
FROM ubuntu:questing
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Ubuntu 25.10 (Questing Quokka)
FROM ubuntu:questing
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Ubuntu 26.04 LTS (Resolute Raccoon)
FROM ubuntu:resolute
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Ubuntu 26.04 LTS (Resolute Raccoon)
FROM ubuntu:resolute
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Debian 13 (Trixie)
FROM debian:trixie-slim
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
#### Base Image: Debian 13 (Trixie)
FROM debian:trixie-slim
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
##################################################### BUILD STAGE ######################################################
FROM photoprism/develop:armv7 AS build
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
##################################################### BUILD STAGE ######################################################
FROM photoprism/develop:bookworm AS build
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
##################################################### BUILD STAGE ######################################################
FROM photoprism/develop:bullseye AS build
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
##################################################### BUILD STAGE ######################################################
FROM photoprism/develop:buster AS build
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
##################################################### BUILD STAGE ######################################################
FROM photoprism/develop:impish AS build
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
##################################################### BUILD STAGE ######################################################
FROM photoprism/develop:jammy AS build
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
##################################################### BUILD STAGE ######################################################
FROM photoprism/develop:lunar AS build
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
##################################################### BUILD STAGE ######################################################
FROM photoprism/develop:mantic AS build
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
##################################################### BUILD STAGE ######################################################
FROM photoprism/develop:noble AS build
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
##################################################### BUILD STAGE ######################################################
FROM photoprism/develop:oracular AS build
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
##################################################### BUILD STAGE ######################################################
FROM photoprism/develop:plucky AS build
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
##################################################### BUILD STAGE ######################################################
FROM photoprism/develop:questing AS build
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,7 +1,7 @@
##################################################### BUILD STAGE ######################################################
FROM photoprism/develop:resolute AS build
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -1,4 +1,4 @@
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/

View file

@ -299,6 +299,16 @@ test.meta("testID", "settings-general-004").meta({ type: "short", mode: "auth" }
await t.expect(photoviewer.markersEditToggle.exists).ok();
await photoviewer.triggerPhotoViewerAction("close-button");
await photo.toggleSelectNthPhoto(0, "image");
await photo.toggleSelectNthPhoto(1, "image");
await contextmenu.triggerContextMenuAction("edit");
await t.expect(photoedit.batchDialog.visible).ok();
await t.expect(photoedit.batchLabels.visible).ok();
await t.click(photoedit.dialogClose);
await contextmenu.clearSelection();
await menu.checkMenuItemAvailability("people", true);
await menu.checkMenuItemAvailability("labels", true);
await menu.openPage("settings");
@ -327,6 +337,16 @@ test.meta("testID", "settings-general-004").meta({ type: "short", mode: "auth" }
await t.expect(photoviewer.markersEditToggle.exists).notOk();
await photoviewer.triggerPhotoViewerAction("close-button");
await photo.toggleSelectNthPhoto(0, "image");
await photo.toggleSelectNthPhoto(1, "image");
await contextmenu.triggerContextMenuAction("edit");
await t.expect(photoedit.batchDialog.visible).ok();
await t.expect(photoedit.batchLabels.exists).notOk();
await t.click(photoedit.dialogClose);
await contextmenu.clearSelection();
await menu.checkMenuItemAvailability("people", false);
await menu.checkMenuItemAvailability("labels", false);
@ -557,7 +577,9 @@ test.meta("testID", "settings-general-006").meta({ type: "short", mode: "auth" }
await photoviewer.checkPhotoViewerActionAvailability("edit-button", false);
await photoviewer.openSidebar();
await photoviewer.assertSidebarIsReadOnly();
// The first non-stacked photo has Title + Labels + Albums but no Caption or People;
// the Filmpreis-anchored block below covers the People positive case.
await photoviewer.assertSidebarIsReadOnly({ expectCaption: false, expectPeople: false });
await photoviewer.triggerPhotoViewerAction("close-button");
await t.expect(Selector("div.p-lightbox__pswp").visible).notOk();

View file

@ -199,9 +199,6 @@ test.meta("testID", "sharing-002").meta({ type: "short", mode: "auth" })("Multi-
});
test.meta("testID", "sharing-003").meta({ type: "short", mode: "auth" })("Common: Lightbox sidebar shows only restricted metadata on share links", async (t) => {
// Reuse the shared album exposed by sharing-002. The Vitest role x
// field matrix already covers the per-role logic against a mocked
// $session; this test pins the real anonymous path end-to-end.
await t.useRole(Role.anonymous());
await t.navigateTo("http://localhost:2343/s/jxoux5ub1e/british-columbia-canada");
await t.expect(toolbar.toolbarSecondTitle.withText("British Columbia").visible).ok();
@ -209,20 +206,13 @@ test.meta("testID", "sharing-003").meta({ type: "short", mode: "auth" })("Common
await photoviewer.openPhotoViewer("nth", 0);
await photoviewer.openSidebar();
// Allow-list: file info and taken-at rows render for anonymous
// viewers so they can tell what they are looking at.
await t.expect(photoviewer.sidebarRow("mdi-calendar").exists).ok();
// Deny-list: edit affordances, face-marker controls, and every
// restricted section must be gone.
await photoviewer.assertSidebarIsReadOnly();
await t.expect(photoviewer.markersVisibilityToggle.exists).notOk();
await t.expect(photoviewer.markersEditToggle.exists).notOk();
// The visitor share-link photo carries a Title but no Caption.
await photoviewer.assertSidebarIsReadOnly({ restricted: true, expectCaption: false });
// Merged file row renders for restricted sessions (type + size as
// the title) but the filename subtitle must be suppressed.
await t.expect(Selector(".p-lightbox-sidebar .meta-file .v-list-item-subtitle").exists).notOk();
await t.expect(photoviewer.sidebarRow("mdi-camera").exists).notOk();
await t.expect(photoviewer.sidebarRow("mdi-camera-iris").exists).notOk();
await t.expect(Selector(".p-lightbox-sidebar .text-subtitle-2").withText("People").exists).notOk();
await t.expect(Selector(".p-lightbox-sidebar .text-subtitle-2").withText("Labels").exists).notOk();
await t.expect(Selector(".p-lightbox-sidebar .text-subtitle-2").withText("Albums").exists).notOk();

View file

@ -67,6 +67,72 @@ test.meta("testID", "sidebar-edit-002").meta({ mode: "public" })("Common: Adds a
await t.expect(Selector("div.is-photo").withAttribute("data-uid", uid).exists).ok();
});
test.meta("testID", "sidebar-edit-004").meta({ mode: "public" })(
"Common: Removes a label and an album inline, undoes before save, and keeps them on the photo",
async (t) => {
const uid = await photoviewer.openSidebarOnFirstPhoto();
const stamp = Date.now();
const labelTitle = `SidebarEditLabelUndo-${stamp}`;
const albumTitle = `SidebarEditAlbumUndo-${stamp}`;
await photoviewer.typeAndConfirmInlineChip("Labels", labelTitle);
await photoviewer.typeAndConfirmInlineChip("Albums", albumTitle);
const labelChip = photoviewer.chipByTitle("Labels", labelTitle);
const albumChip = photoviewer.chipByTitle("Albums", albumTitle);
await photoviewer.removeInlineChip("Labels", labelTitle);
await t.expect(labelChip.exists).notOk();
await photoviewer.removeInlineChip("Albums", albumTitle);
await t.expect(albumChip.exists).notOk();
await photoviewer.undoChipRemovals("Labels");
await t.expect(labelChip.exists).ok();
await photoviewer.undoChipRemovals("Albums");
await t.expect(albumChip.exists).ok();
// Close + reopen rehydrates the sidebar from the API; both chips must
// still be there because Undo never reached the backend.
await photoviewer.triggerPhotoViewerAction("close-button");
await photoviewer.openSidebarOnPhoto(uid);
await t.expect(photoviewer.chipByTitle("Labels", labelTitle).exists).ok();
await t.expect(photoviewer.chipByTitle("Albums", albumTitle).exists).ok();
}
);
test.meta("testID", "sidebar-edit-005").meta({ mode: "public" })(
"Common: Removes a label and an album inline, saves the removal, and detaches them from the photo",
async (t) => {
const uid = await photoviewer.openSidebarOnFirstPhoto();
const stamp = Date.now();
const labelTitle = `SidebarEditLabelSave-${stamp}`;
const albumTitle = `SidebarEditAlbumSave-${stamp}`;
await photoviewer.typeAndConfirmInlineChip("Labels", labelTitle);
await photoviewer.typeAndConfirmInlineChip("Albums", albumTitle);
await photoviewer.removeInlineChip("Labels", labelTitle);
await photoviewer.removeInlineChip("Albums", albumTitle);
await photoviewer.confirmChipRemovals("Labels");
await photoviewer.confirmChipRemovals("Albums");
await t.expect(photoviewer.chipByTitle("Labels", labelTitle).exists).notOk();
await t.expect(photoviewer.chipByTitle("Albums", albumTitle).exists).notOk();
// Close + reopen confirms the removal persisted past the round-trip;
// the saved photo no longer references either chip.
await photoviewer.triggerPhotoViewerAction("close-button");
await photoviewer.openSidebarOnPhoto(uid);
await t.expect(photoviewer.chipByTitle("Labels", labelTitle).exists).notOk();
await t.expect(photoviewer.chipByTitle("Albums", albumTitle).exists).notOk();
}
);
test.meta("testID", "sidebar-edit-003").meta({ mode: "public" })(
"Common: Edits every taken-at, camera, and location field and confirms persistence",
async (t) => {

View file

@ -30,6 +30,7 @@ export default class Page {
this.batchDialogToolbarCloseAction = Selector("div.v-dialog--batch-edit header.v-toolbar button.action-close");
this.batchToggleAllCheckbox = Selector("div.v-dialog--batch-edit .toggle-all div.v-selection-control__input");
this.batchToggleSelectCheckbox = Selector("div.v-dialog--batch-edit .toggle-select div.v-selection-control__input");
this.batchLabels = Selector("div.v-dialog--batch-edit .input-labels", { timeout: 15000 });
this.detailsDone = Selector(".p-form-photo-details-meta button.action-done", {
timeout: 15000,

View file

@ -107,10 +107,7 @@ export default class Page {
// typeAndConfirmInlineChip adds a Labels or Albums chip via the always-rendered combobox.
// The short wait lets Vuetify's combobox seat the typed value before Enter commits it.
async typeAndConfirmInlineChip(sectionLabel, value) {
const sectionClass = sectionLabel === "Labels" ? "meta-labels" : sectionLabel === "Albums" ? "meta-albums" : null;
if (!sectionClass) {
throw new Error(`Unknown chip section: ${sectionLabel}`);
}
const sectionClass = this._chipSectionClass(sectionLabel);
const input = Selector(`.p-lightbox-sidebar .${sectionClass} .meta-inline-edit input`, { timeout: 15000 });
await t.click(input).typeText(input, value);
await t.wait(200);
@ -118,6 +115,42 @@ export default class Page {
await t.expect(Selector(".meta-inline-menu").exists).notOk();
}
// chipByTitle returns the .meta-chip in the Labels or Albums section matching `title`.
chipByTitle(sectionLabel, title) {
const sectionClass = this._chipSectionClass(sectionLabel);
return Selector(`.p-lightbox-sidebar .${sectionClass}.metadata__chips .meta-chip`, { timeout: 15000 }).withText(title);
}
// removeInlineChip clicks the × on a Labels or Albums chip matching `title`.
// Soft-removes locally via chipState.<field>.removals until confirm or undo.
async removeInlineChip(sectionLabel, title) {
await t.click(this.chipByTitle(sectionLabel, title).find(".meta-chip__remove"));
}
// undoChipRemovals clicks Undo in the Labels or Albums chip toolbar,
// restoring all soft-removed chips for that section.
async undoChipRemovals(sectionLabel) {
const sectionClass = this._chipSectionClass(sectionLabel);
await t.click(Selector(`.p-lightbox-sidebar .${sectionClass} .meta-chip-undo`, { timeout: 15000 }));
}
// confirmChipRemovals clicks Save in the Labels or Albums chip toolbar,
// persisting all pending removals.
async confirmChipRemovals(sectionLabel) {
const sectionClass = this._chipSectionClass(sectionLabel);
await t.click(Selector(`.p-lightbox-sidebar .${sectionClass} .meta-chip-confirm`, { timeout: 15000 }));
}
_chipSectionClass(sectionLabel) {
if (sectionLabel === "Labels") {
return "meta-labels";
}
if (sectionLabel === "Albums") {
return "meta-albums";
}
throw new Error(`Unknown chip section: ${sectionLabel}`);
}
// startInlineEditOrAdd enters edit mode for Title or Caption by clicking the row, or
// falls back to the add-prompt when the field is empty.
async startInlineEditOrAdd(displayClass, promptLabel) {
@ -164,6 +197,16 @@ export default class Page {
// editor or dialog. Paired with assertSidebarIsReadOnly() so every selector has both a
// positive and a negative assertion.
async assertSidebarIsEditable() {
// Title and Caption render unconditionally in editable mode; click each
// to open its inline editor and escape back out before continuing.
await this.startInlineEditOrAdd("meta-title", "Add a Title");
await t.expect(Selector(".p-lightbox-sidebar .meta-inline-title").visible).ok();
await t.pressKey("esc");
await this.startInlineEditOrAdd("meta-caption", "Add a Caption");
await t.expect(Selector(".p-lightbox-sidebar .meta-inline-caption").visible).ok();
await t.pressKey("esc");
await t.click(this.sidebarRow("mdi-calendar"));
await t.expect(this.dateTimeDialog.root.visible).ok();
await t.click(this.dateTimeDialog.cancel);
@ -180,6 +223,18 @@ export default class Page {
await t.click(this.locationDialog.cancel);
}
// People, Albums, and Labels: assert their editable-only controls render.
// The pencil toggle and chip-add inputs only exist when isEditable=true,
// so they discriminate edit mode from read-only mode (which uses the eye
// toggle and renders chips without an add input).
await t.expect(this.peopleHeader.exists).ok();
await t.expect(this.markersEditToggle.exists).ok();
await t.expect(this.markersVisibilityToggle.exists).notOk();
await t.expect(Selector(".p-lightbox-sidebar .v-list-item.meta-albums").exists).ok();
await t.expect(Selector(".p-lightbox-sidebar .meta-albums .meta-inline-edit").exists).ok();
await t.expect(Selector(".p-lightbox-sidebar .v-list-item.meta-labels").exists).ok();
await t.expect(Selector(".p-lightbox-sidebar .meta-labels .meta-inline-edit").exists).ok();
// The next iteration's row click cancels the previous edit, so only the trailing
// toolbar click is needed for cleanup. Gate on .visible so rows that v-show hides
// for empty fields are skipped.
@ -194,19 +249,76 @@ export default class Page {
await t.click(Selector(".p-lightbox-sidebar .v-toolbar-title"));
}
// assertSidebarIsReadOnly is the inverse of assertSidebarIsEditable.
async assertSidebarIsReadOnly() {
// assertSidebarIsReadOnly verifies the sidebar surfaces no editors or dialogs.
// restricted=true asserts the visitor subset (Title, Caption, Date, Location).
// expect* flags toggle positive existence checks; defaults assert presence.
async assertSidebarIsReadOnly({
restricted = false,
expectTitle = true,
expectCaption = true,
expectPeople = true,
expectLabels = true,
expectAlbums = true,
} = {}) {
await t.click(this.sidebarRow("mdi-calendar"));
await t.expect(this.dateTimeDialog.root.visible).notOk();
if (await this.sidebarRow("mdi-map-marker").exists) {
await t.click(this.sidebarRow("mdi-map-marker"));
await t.expect(this.locationDialog.root.visible).notOk();
}
for (const [key, expected] of [
["title", expectTitle],
["caption", expectCaption],
]) {
const display = Selector(`.p-lightbox-sidebar .meta-${key}`);
if (expected) {
await t.expect(display.exists).ok();
const row = display.parent(".v-list-item");
await t.click(row);
await t.expect(row.find(".meta-inline-edit").exists).notOk();
} else {
await t.expect(display.exists).notOk();
}
}
// meta-add-prompt is an editing affordance and must never render here.
await t.expect(Selector(".p-lightbox-sidebar .meta-add-prompt").visible).notOk();
if (restricted) {
await t.expect(this.sidebarRow("mdi-camera").exists).notOk();
await t.expect(this.sidebarRow("mdi-camera-iris").exists).notOk();
await t.expect(this.peopleHeader.exists).notOk();
await t.expect(Selector(".p-lightbox-sidebar .metadata__person-row").exists).notOk();
await t.expect(this.markersVisibilityToggle.exists).notOk();
await t.expect(this.markersEditToggle.exists).notOk();
await t.expect(Selector(".p-lightbox-sidebar .meta-albums").exists).notOk();
await t.expect(Selector(".p-lightbox-sidebar .meta-labels").exists).notOk();
for (const key of ["subject", "artist", "copyright", "license", "keywords", "notes"]) {
await t.expect(Selector(`.p-lightbox-sidebar .v-list-item.meta-${key}`).exists).notOk();
}
return;
}
if (await this.sidebarRow("mdi-camera").exists) {
await t.click(this.sidebarRow("mdi-camera"));
await t.expect(this.cameraDialog.root.visible).notOk();
}
if (await this.sidebarRow("mdi-map-marker").exists) {
await t.click(this.sidebarRow("mdi-map-marker"));
await t.expect(this.locationDialog.root.visible).notOk();
// Mirror the editable assertions: section renders, but with read-only
// controls only — eye toggle for People, no chip-add input for Albums/Labels.
if (expectPeople) {
await t.expect(this.peopleHeader.exists).ok();
await t.expect(this.markersVisibilityToggle.exists).ok();
await t.expect(this.markersEditToggle.exists).notOk();
}
if (expectAlbums) {
await t.expect(Selector(".p-lightbox-sidebar .v-list-item.meta-albums").exists).ok();
await t.expect(Selector(".p-lightbox-sidebar .meta-albums .meta-inline-edit").exists).notOk();
}
if (expectLabels) {
await t.expect(Selector(".p-lightbox-sidebar .v-list-item.meta-labels").exists).ok();
await t.expect(Selector(".p-lightbox-sidebar .meta-labels .meta-inline-edit").exists).notOk();
}
for (const key of ["subject", "artist", "copyright", "license", "keywords", "notes"]) {

View file

@ -1,17 +1,42 @@
## PhotoPrism — NSFW Package
**Last Updated:** December 23, 2025
**Last Updated:** May 21, 2026
### Overview
`internal/ai/nsfw` runs the built-in TensorFlow NSFW classifier to score images for drawing, hentai, neutral, porn, and sexy content. It is used during indexing and metadata workflows when the NSFW model is enabled.
`internal/ai/nsfw` runs the built-in TensorFlow NSFW classifier to score images for drawing, hentai, neutral, porn, and sexy content. It is the default backend that powers the `Type: nsfw` model entry in [`internal/ai/vision`](../vision/README.md) and is the only NSFW engine that ships with PhotoPrism out of the box; operators can override it through `vision.yml` with an Ollama or OpenAI endpoint when they prefer to run NSFW detection on a remote LLM.
### Where It Gets Called
The package itself only exposes the model loader and a thin scoring API (`Result.IsSafe`, `Result.IsNsfw(threshold)`). Two upstream callers wire it into the runtime:
1. **Upload handler — [`internal/api/users_upload.go`](../../api/users_upload.go).** When `PHOTOPRISM_UPLOAD_NSFW=false` (default `true`), every accepted upload is screened by `vision.DetectNSFW` before indexing. Files that score above the threshold are deleted on the spot — they never reach `originals/`. When `UPLOAD_NSFW=true`, the upload path skips the check entirely.
2. **Index + vision-worker pipelines — [`internal/photoprism/index_mediafile.go`](../../photoprism/index_mediafile.go), [`internal/workers/vision.go`](../../workers/vision.go), [`internal/workers/meta.go`](../../workers/meta.go).** When `PHOTOPRISM_DETECT_NSFW=true` (default `false`), the indexer marks new photos as `PhotoPrivate = true` if the NSFW model flags them. Both code paths short-circuit when `DetectNSFW()` is false — the model is then neither loaded nor invoked.
Both flags are independent: you can reject uploads without flagging existing imports, flag existing imports without policing uploads, or both. The user-facing matrix lives at [docs.photoprism.app/user-guide/ai/nsfw/](https://docs.photoprism.app/user-guide/ai/nsfw/).
### Detection Through the Labels Model
When `Type: labels` is served by an Ollama or OpenAI engine and **both** `PHOTOPRISM_DETECT_NSFW=true` and `PHOTOPRISM_EXPERIMENTAL=true` are set, [`internal/config/config.go`](../../config/config.go) flips the package-level global `vision.DetectNSFWLabels` to `true`. The Ollama and OpenAI engine builders then swap their default label prompts for `LabelPromptNSFW` and the JSON schema generators add `nsfw` + `nsfw_confidence` fields, so NSFW classification piggybacks on the label-generation call instead of running as a separate inference pass.
When the shortcut is active, the labels-path check in `index_mediafile.go` (`labels.IsNSFW(threshold)`) can promote a photo to private without this package being touched. The dedicated TensorFlow model in `internal/ai/nsfw` is still used as a fallback whenever the labels path either does not run or does not return NSFW signals, and whenever `vision run --models nsfw` is invoked directly.
### How It Works
- **Model Loading** — Loads the NSFW SavedModel from `assets/models/` and resolves input/output ops (inferred if missing).
- **Input Preparation** — JPEG images are decoded and transformed to the configured input resolution.
- **Input Preparation** — JPEG thumbnails (default size `Fit720`, see `MediaFile.DetectNSFW`) are decoded and transformed to the configured input resolution.
- **Inference & Output** — Produces five class probabilities mapped into a `Result` struct for downstream thresholds and UI badges.
### Threshold
`vision.yml` carries a `Thresholds.NSFW` value (default `75`, range `0-100`) that controls how confident the model must be before a picture is flagged. Lower values are more aggressive; higher values more permissive. The threshold applies to both the dedicated NSFW model and the NSFW fields returned via the label-generation shortcut.
```yaml
Thresholds:
NSFW: 75
```
### Memory & Performance
TensorFlow tensors allocate C memory and are freed by Go GC finalizers. To keep RSS bounded during long runs, PhotoPrism periodically triggers garbage collection to return freed tensor memory to the OS. Tune with:
@ -24,8 +49,13 @@ TensorFlow tensors allocate C memory and are freed by Go GC finalizers. To keep
- **Model fails to load:** Verify `saved_model.pb` and `variables/` exist under the model path.
- **Unexpected scores:** Confirm the input resolution matches the model and that logits are handled correctly.
- **High memory usage:** Adjust `PHOTOPRISM_TF_GC_EVERY` or reduce concurrent indexing load.
- **NSFW detection appears to stop working after switching labels to an LLM:** Confirm both `PHOTOPRISM_DETECT_NSFW=true` and `PHOTOPRISM_EXPERIMENTAL=true` are set. Without both, the labels-path shortcut is disabled and only an explicit `vision run --models nsfw` (or another caller that goes through this package directly) will produce NSFW flags.
### Related Docs
- [`internal/ai/vision/README.md`](../vision/README.md) — model registry and run scheduling
- [`internal/ai/vision/README.md`](../vision/README.md) — model registry, run scheduling, and the `DetectNSFWLabels` global
- [`internal/ai/vision/ollama/README.md`](../vision/ollama/README.md) — Ollama engine: `LabelPromptNSFW` swap-in
- [`internal/ai/vision/openai/README.md`](../vision/openai/README.md) — OpenAI engine: NSFW-aware prompt and schema
- [`internal/ai/vision/schema/README.md`](../vision/schema/README.md) — JSON schema variants used when NSFW is enabled
- [`internal/ai/tensorflow/README.md`](../tensorflow/README.md) — TensorFlow helpers, GC behavior, and model loading
- [docs.photoprism.app/user-guide/ai/nsfw/](https://docs.photoprism.app/user-guide/ai/nsfw/) — user-facing reference + flag matrix

View file

@ -1,6 +1,6 @@
## PhotoPrism — Vision Package
**Last Updated:** March 3, 2026
**Last Updated:** May 21, 2026
### Overview
@ -201,6 +201,14 @@ Models:
- **Ollama**: private, GPU/CPU-hosted multimodal LLMs; best for richer captions/labels without cloud traffic.
- **OpenAI**: highest quality reasoning and multimodal support; requires API key and network access.
### NSFW Detection
NSFW is wired through the same model registry as labels, captions, and faces — `Type: nsfw` resolves to the built-in TensorFlow classifier by default, and can be overridden in `vision.yml` to point at an Ollama or OpenAI endpoint.
There is also a fast-path: when `Type: labels` is served by an LLM, PhotoPrism can ask the labels call to include `nsfw` + `nsfw_confidence` in the same response. This is gated by the package-level global `DetectNSFWLabels`, set from `config.go` as `DetectNSFW() && Experimental()` — both `PHOTOPRISM_DETECT_NSFW=true` **and** `PHOTOPRISM_EXPERIMENTAL=true` are required. When either flag is off, the labels prompt stays on `LabelPromptDefault` (no NSFW fields), and `labels.IsNSFW()` cannot trigger.
The runtime guards in `internal/photoprism/index_mediafile.go` and `internal/workers/vision.go` additionally short-circuit any NSFW promotion on `conf.DetectNSFW()`. The dedicated `Type: nsfw` model is filtered out of scheduled runs by `VisionModelShouldRun` whenever `DetectNSFW()` is false. See [`internal/ai/nsfw/README.md`](../nsfw/README.md) for the full call-graph and the user-facing matrix at [docs.photoprism.app/user-guide/ai/nsfw/](https://docs.photoprism.app/user-guide/ai/nsfw/).
### Model Unload on Idle
PhotoPrism currently keeps TensorFlow models resident for the lifetime of the process to avoid repeated load costs. A future “model unload on idle” mode would track last-use timestamps and close the TensorFlow session/graph after a configurable idle period, releasing the models memory footprint back to the OS. The trade-off is higher latency and CPU overhead when a model is used again, plus extra I/O to reload weights. This may be attractive for low-frequency or memory-constrained deployments but would slow continuous indexing jobs, so it is not enabled today.

View file

@ -1,6 +1,6 @@
## PhotoPrism — Ollama Engine Integration
**Last Updated:** February 23, 2026
**Last Updated:** May 21, 2026
### Overview
@ -41,7 +41,7 @@ This package provides PhotoPrisms native adapter for Ollama-compatible multim
- Captions: no system prompt by default; rely on user prompt or set one explicitly for stylistic needs.
- **User Prompts**
- Captions use `CaptionPrompt`, which requests one sentence in active voice.
- Labels default to `LabelPromptDefault`; when `DetectNSFWLabels` is true, the adapter swaps in `LabelPromptNSFW`.
- Labels default to `LabelPromptDefault`; when the package-level `DetectNSFWLabels` global is true, the adapter swaps in `LabelPromptNSFW`. The global is set by `config.go` to `DetectNSFW() && Experimental()`, so both `PHOTOPRISM_DETECT_NSFW=true` and `PHOTOPRISM_EXPERIMENTAL=true` are required to enable the NSFW-aware prompt.
- For stricter noun enforcement, set `Prompt` to `LabelPromptStrict`.
- **Schemas**
- Labels rely on `schema.LabelsJson(nsfw)` (simple JSON template). Setting `Format: json` auto-attaches a reminder (`model.SchemaInstructions()`).

View file

@ -1,6 +1,6 @@
## PhotoPrism — OpenAI API Integration
**Last Updated:** November 14, 2025
**Last Updated:** May 21, 2026
### Overview
@ -28,7 +28,7 @@ This package contains PhotoPrisms adapter for the OpenAI Responses API. It en
### Prompt, Model, & Schema Guidance
- **Models:** The adapter targets GPT5 vision tiers (e.g. `gpt-5-nano`, `gpt-5-mini`). These models support image inputs, structured outputs, and deterministic settings. Set `Name` to the exact provider identifier so defaults are applied correctly. Caption models share the same configuration surface and run through the same adapter.
- **Prompts:** Defaults live in `defaults.go`. Captions use a single-sentence instruction; labels use `LabelPromptDefault` (or `LabelPromptNSFW` when PhotoPrism requests NSFW metadata). Custom prompts should retain schema reminders so structured outputs stay valid.
- **Prompts:** Defaults live in `defaults.go`. Captions use a single-sentence instruction; labels use `LabelPromptDefault` (or `LabelPromptNSFW` when the package-level `vision.DetectNSFWLabels` global is true, which requires both `PHOTOPRISM_DETECT_NSFW=true` and `PHOTOPRISM_EXPERIMENTAL=true`). Custom prompts should retain schema reminders so structured outputs stay valid.
- **Schemas:** Labels use the JSON schema returned by `schema.LabelsJsonSchema(nsfw)`; the response format name is derived via `schema.JsonSchemaName` (e.g. `photoprism_vision_labels_v1`). Captions omit schemas unless operators explicitly request a structured format.
- **When to keep defaults:** For most deployments, leaving `System`, `Prompt`, `Schema`, and `Options` unset yields stable output with minimal configuration. Override them only when domain-specific language or custom scoring is necessary, and add regression tests alongside.

View file

@ -1,6 +1,6 @@
## PhotoPrism — Vision Schema Reference
**Last Updated:** November 14, 2025
**Last Updated:** May 21, 2026
### Overview
@ -13,12 +13,12 @@ Both helpers build on the same field set (`name`, `confidence`, `topicality`, an
### Schema Types & Differences
| Helper | Target Engine | Format | Validation Style | When To Use |
|:--------------------------|:-------------------------|:-------------------------------------------------------|:------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------|
| `LabelsJsonSchema(false)` | OpenAI (standard labels) | JSON Schema Draft | Strong: OpenAI enforces field types/ranges server-side before returning a response. | When calling GPTvision models via `ApiFormatOpenAI` to ensure PhotoPrism receives well-formed label arrays. |
| `LabelsJsonSchema(true)` | OpenAI (labels + NSFW) | JSON Schema Draft with additional boolean/float fields | Strong: same enforcement plus required NSFW fields. | When `DetectNSFWLabels` or NSFW-specific prompts are active and the model must emit `nsfw` + `nsfw_confidence`. |
| `LabelsJson(false)` | Ollama (standard labels) | Plain JSON example | Soft: model is nudged to mimic the structure through prompt instructions. | When running self-hosted Ollama models that support “JSON mode” but do not consume JSON Schema definitions. |
| `LabelsJson(true)` | Ollama (labels + NSFW) | Plain JSON example with NSFW keys | Soft: prompts describe the required keys; the adapter validates after parsing. | When Ollama prompts mention NSFW scoring or PhotoPrism sets `DetectNSFWLabels=true`. |
| Helper | Target Engine | Format | Validation Style | When To Use |
|:--------------------------|:-------------------------|:-------------------------------------------------------|:------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `LabelsJsonSchema(false)` | OpenAI (standard labels) | JSON Schema Draft | Strong: OpenAI enforces field types/ranges server-side before returning a response. | When calling GPTvision models via `ApiFormatOpenAI` to ensure PhotoPrism receives well-formed label arrays. |
| `LabelsJsonSchema(true)` | OpenAI (labels + NSFW) | JSON Schema Draft with additional boolean/float fields | Strong: same enforcement plus required NSFW fields. | When `DetectNSFWLabels` (gated on `DETECT_NSFW=true && EXPERIMENTAL=true`) or NSFW-specific prompts are active and the model must emit `nsfw` + `nsfw_confidence`. |
| `LabelsJson(false)` | Ollama (standard labels) | Plain JSON example | Soft: model is nudged to mimic the structure through prompt instructions. | When running self-hosted Ollama models that support “JSON mode” but do not consume JSON Schema definitions. |
| `LabelsJson(true)` | Ollama (labels + NSFW) | Plain JSON example with NSFW keys | Soft: prompts describe the required keys; the adapter validates after parsing. | When Ollama prompts mention NSFW scoring or PhotoPrism sets `DetectNSFWLabels=true`. |
**Key technical distinction:** OpenAIs Responses API accepts a JSON Schema (see `LabelsJsonSchema*`) and guarantees compliance by rejecting invalid responses, while Ollama currently relies on prompt-directed output. For Ollama integrations we provide a representative JSON document (`LabelsJson*`) that models can imitate; PhotoPrism then normalizes and validates the results in Go.

View file

@ -30,6 +30,7 @@ The API package exposes PhotoPrisms HTTP endpoints via Gin handlers. Each fil
- Enforce rate limiting with the shared limiters (`limiter.Auth`, `limiter.Login`) and respond with `limiter.AbortJSON` to maintain consistent 429 JSON payloads.
- Derive client IPs through `api.ClientIP` and extract bearer tokens with `header.BearerToken` or the helper setters. Use constant-time comparison for tokens and secrets.
- For downloads or proxy endpoints, validate URLs against allowed schemes (`http`, `https`) and reject private or loopback addresses unless explicitly required.
- **Upload-time NSFW screening (`users_upload.go`)** — when `PHOTOPRISM_UPLOAD_NSFW=false`, the upload handler runs `vision.DetectNSFW` against every accepted file and deletes any file flagged above the NSFW threshold before it reaches `originals/`. The check is skipped entirely when `UPLOAD_NSFW=true` (default). See [`internal/ai/nsfw/README.md`](../ai/nsfw/README.md) for the full NSFW call-graph and flag matrix.
### Audit Logging

View file

@ -20,6 +20,24 @@ The `commands` package hosts the CLI implementation for the PhotoPrism binary. C
- When integrating configuration options, call the accessors on `*config.Config` (for example, `conf.ClusterUUID()`) rather than mutating option structs directly.
- For HTTP interactions, depend on the safe download helpers in `pkg/http/safe` or the specialized wrappers in `internal/thumb/avatar` to inherit timeout, size, and SSRF protection defaults.
### Positional Arguments & Flag Order
`urfave/cli` v2 delegates flag parsing to the Go stdlib `flag` package, which **stops parsing at the first non-flag token**. For any subcommand that takes a positional argument (for example `photoprism users mod USERNAME --role guest`), flags placed **after** the positional are not parsed — they are returned as additional positionals and `ctx.IsSet(...)` reports `false` for each of them.
Without guarding for this, an action that conditionally applies values via `if ctx.IsSet("role") { ... }` will silently no-op while still logging success.
#### Mitigation Helper
Call `commands.RejectTrailingFlags(ctx)` near the top of every leaf action whose CLI shape is `Action <positional> [--flags...]`. The helper returns a clear `flag "--name" must appear before positional arguments` error when it detects a flag-like token in `ctx.Args().Tail()`, so the user is told to re-order rather than seeing a silent no-op. Pair it with `commands.TrailingFlagToken(ctx)` when the action needs to inspect the offending token before deciding what to do.
Helper behavior:
- Single-dash `-` and the `--` terminator are treated as positionals, not flags, so commands like `photoprism backup -` (write to stdout) and `photoprism foo bar -- --literal` keep working.
- Unknown flags placed before the positional are not the helper's concern — `urfave/cli` raises its own "flag provided but not defined" error in that path.
- For commands that use a flag-based identifier instead of a positional, still call the helper after the existence check so trailing flags surface as a usage error rather than a silent ignore.
The underlying parser limitation is tracked as a known issue for a broader fix; until a global arg-reorder pass lands, all new leaf actions that accept a positional MUST call `RejectTrailingFlags` before applying flag values.
### Configuration & Flags Integration
- Define new options in `internal/config/options.go` with the appropriate struct tags (`yaml`, `json`, `flag`) so they propagate to YAML, CLI, and API layers consistently.

View file

@ -48,6 +48,12 @@ func authAddAction(ctx *cli.Context) error {
// Get username from command flag.
userName := clean.Username(ctx.Args().First())
// Reject flags placed after the username; the stdlib flag parser
// would silently drop them and create the token with the defaults.
if err := RejectTrailingFlags(ctx); err != nil {
return err
}
// Find user account.
user := entity.FindUserByName(userName)

View file

@ -0,0 +1,37 @@
package commands
import (
"fmt"
"strings"
"github.com/urfave/cli/v2"
)
// TrailingFlagToken returns the first arg after the leading positional that
// looks like a CLI flag (e.g. "--name" or "-r").
//
// urfave/cli v2 delegates flag parsing to the stdlib flag package, which
// stops at the first non-flag token; flags placed after a positional are
// silently ignored. Callers use this helper to reject such invocations
// instead of acting on a context with the supplied flag values dropped.
func TrailingFlagToken(ctx *cli.Context) string {
for _, a := range ctx.Args().Tail() {
if a == "--" {
return ""
}
if strings.HasPrefix(a, "-") && len(a) > 1 {
return a
}
}
return ""
}
// RejectTrailingFlags returns a usage error when flag-like tokens appear after
// the leading positional argument, naming the first offending token so users
// can fix the invocation rather than seeing a silent no-op.
func RejectTrailingFlags(ctx *cli.Context) error {
if tok := TrailingFlagToken(ctx); tok != "" {
return fmt.Errorf("flag %q must appear before positional arguments", tok)
}
return nil
}

View file

@ -0,0 +1,56 @@
package commands
import (
"flag"
"testing"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2"
)
func TestTrailingFlagToken(t *testing.T) {
cases := []struct {
name string
args []string
want string
}{
{"NoTail", []string{"alice"}, ""},
{"OnlyPositionals", []string{"alice", "bob"}, ""},
{"LongFlagAfterPositional", []string{"alice", "--name", "Alicia"}, "--name"},
{"EqualsFlagAfterPositional", []string{"alice", "--role=guest"}, "--role=guest"},
{"ShortFlagAfterPositional", []string{"alice", "-r", "guest"}, "-r"},
{"BareDashIsPositional", []string{"alice", "-"}, ""},
{"DoubleDashStopsScan", []string{"alice", "--", "--name", "literal"}, ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
set := flag.NewFlagSet("test", flag.ContinueOnError)
if err := set.Parse(tc.args); err != nil {
t.Fatalf("flag parse: %v", err)
}
ctx := cli.NewContext(nil, set, nil)
assert.Equal(t, tc.want, TrailingFlagToken(ctx))
})
}
}
func TestRejectTrailingFlags(t *testing.T) {
t.Run("NoTrailingFlag", func(t *testing.T) {
set := flag.NewFlagSet("test", flag.ContinueOnError)
_ = set.Parse([]string{"alice"})
ctx := cli.NewContext(nil, set, nil)
assert.NoError(t, RejectTrailingFlags(ctx))
})
t.Run("TrailingFlagReturnsError", func(t *testing.T) {
set := flag.NewFlagSet("test", flag.ContinueOnError)
_ = set.Parse([]string{"alice", "--name", "Alicia"})
ctx := cli.NewContext(nil, set, nil)
err := RejectTrailingFlags(ctx)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "--name")
assert.Contains(t, err.Error(), "must appear before positional arguments")
}
})
}

View file

@ -31,6 +31,12 @@ func clientsAddAction(ctx *cli.Context) error {
return CallWithDependencies(ctx, func(conf *config.Config) error {
conf.MigrateDb(false, nil)
// Reject flags placed after the username; the stdlib flag parser
// would silently drop them and register the client with the defaults.
if err := RejectTrailingFlags(ctx); err != nil {
return err
}
frm := form.AddClientFromCli(ctx)
interactive := true

View file

@ -34,6 +34,12 @@ func clientsModAction(ctx *cli.Context) error {
return cli.ShowSubcommandHelp(ctx)
}
// Reject flags placed after the client id; the stdlib flag parser
// would silently drop them and report "client updated" with no changes.
if err := RejectTrailingFlags(ctx); err != nil {
return err
}
// Find client record.
client := entity.FindClientByUID(frm.ID())

View file

@ -28,6 +28,12 @@ func usersAddAction(ctx *cli.Context) error {
return CallWithDependencies(ctx, func(conf *config.Config) error {
conf.MigrateDb(false, nil)
// Reject flags placed after the username; the stdlib flag parser
// would silently drop them and create the account with the defaults.
if err := RejectTrailingFlags(ctx); err != nil {
return err
}
frm := form.NewUserFromCli(ctx)
interactive := true

View file

@ -37,6 +37,12 @@ func usersModAction(ctx *cli.Context) error {
return cli.ShowSubcommandHelp(ctx)
}
// Reject flags placed after the username; the stdlib flag parser
// would silently drop them and report "user updated" with no changes.
if err := RejectTrailingFlags(ctx); err != nil {
return err
}
// Find user record.
var m *entity.User

View file

@ -4,6 +4,9 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/photoprism/photoprism/internal/entity"
)
func TestUsersModCommand(t *testing.T) {
@ -25,4 +28,22 @@ func TestUsersModCommand(t *testing.T) {
assert.Error(t, err)
assert.Empty(t, output)
})
t.Run("RejectFlagsAfterPositional", func(t *testing.T) {
// Run with the broken arg order QA reported (positional first, then flags).
// The stdlib flag parser stops at "alice", so --name / --role would
// silently no-op without RejectTrailingFlags.
output, err := RunWithTestContext(UsersModCommand, []string{"mod", "alice", "--name", "Alicia", "--role", "guest"})
require.Error(t, err)
assert.Contains(t, err.Error(), "must appear before positional arguments")
assert.Empty(t, output)
// Confirm the alice fixture is untouched when it still exists. Earlier
// tests in the suite may have deleted it, so skip the comparison in
// that case rather than coupling this test to suite ordering.
if alice := entity.FindUserByName("alice"); alice != nil {
assert.Equal(t, "Alice", alice.DisplayName)
assert.Equal(t, "admin", alice.UserRole)
}
})
}

View file

@ -289,7 +289,7 @@ var Flags = CliFlags{
Flag: &cli.BoolFlag{
Name: "upload-nsfw",
Aliases: []string{"n"},
Usage: "allows uploads that might be offensive (detecting unsafe content requires TensorFlow)",
Usage: "allows uploads that might be offensive (when disabled, files flagged by the NSFW model are rejected before indexing)",
EnvVars: EnvVars("UPLOAD_NSFW"),
}}, {
Flag: &cli.StringFlag{
@ -1225,7 +1225,7 @@ var Flags = CliFlags{
}}, {
Flag: &cli.BoolFlag{
Name: "detect-nsfw",
Usage: "flags newly added pictures as private if they might be offensive (requires TensorFlow)",
Usage: "flags newly added pictures as private if they might be offensive (uses the configured NSFW model; built-in TensorFlow by default)",
EnvVars: EnvVars("DETECT_NSFW"),
}}, {
Flag: &cli.StringFlag{

View file

@ -1,6 +1,6 @@
## PhotoPrism — Core Package
**Last Updated:** March 3, 2026
**Last Updated:** May 21, 2026
### Overview
@ -42,6 +42,7 @@
- Import: run via `ImportWorker` with `ImportOptions`; stacked handling is driven by metadata and document IDs.
- Converters: use `Convert.ToImage` / `Convert.ToVideo` / `Convert.ToJson`; options come from `config.Config`.
- Vision: thumbnails for vision models are selected in `mediafile_vision.go`; ensure models exist in `internal/ai/vision`.
- NSFW: `index_mediafile.go` flags new photos as `PhotoPrivate` when the labels-path NSFW shortcut (LLM with `DETECT_NSFW=true && EXPERIMENTAL=true`) hits or, as a fallback, when `m.DetectNSFW()` returns true and `PHOTOPRISM_DETECT_NSFW=true`. Both promotions short-circuit when `DetectNSFW()` is false. Full call-graph + flag matrix in [`internal/ai/nsfw/README.md`](../ai/nsfw/README.md).
- Tests: targeted runs keep iteration fast, e.g.
- `go test ./internal/photoprism -run TestMediaFile_ -count=1`
- `go test ./internal/photoprism/index_mediafile_test.go -run TestIndexMediaFile`

View file

@ -1,7 +1,7 @@
/*
Package security provides shared HTTP security utilities.
Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
Questions? Email us at hello@photoprism.app or visit our website to learn
more about our team, products and services: https://www.photoprism.app/

View file

@ -0,0 +1,43 @@
apiVersion: v2
name: photoprism-plus
type: application
description: >-
Deploys PhotoPrism® on Kubernetes with optional portal integration,
persistent storage, and configurable authentication backends.
version: "1.1.0"
appVersion: "260305"
home: https://www.photoprism.app/
icon: https://dl.photoprism.app/icons/logo/256.png
maintainers:
- name: PhotoPrism UG
url: https://www.photoprism.app/contact
keywords:
- photos
- photoprism
- ai
- media
- dam
- webdav
- oidc
sources:
- https://docs.photoprism.app/
- https://www.photoprism.app/kb
- https://www.photoprism.app/kb/getting-support
- https://github.com/photoprism/photoprism
annotations:
catalog.cattle.io/display-name: "PhotoPrism®"
catalog.cattle.io/type: "app"
catalog.cattle.io/category: "Digital Asset Management (DAM)"
catalog.cattle.io/requests-cpu: "1000m"
catalog.cattle.io/requests-memory: "1Gi"
catalog.cattle.io/os: "linux"
catalog.cattle.io/featured: "true"
artifacthub.io/license: "AGPL-3.0-or-later"
artifacthub.io/category: "storage"
artifacthub.io/links: |
- name: Documentation
url: https://docs.photoprism.app/
- name: Knowledge Base
url: https://www.photoprism.app/kb
- name: Support
url: https://www.photoprism.app/kb/getting-support

View file

@ -0,0 +1,93 @@
## Introduction
This chart provides an easy way to run [PhotoPrism®](https://www.photoprism.app/editions#compare) on a home lab or small business Kubernetes cluster while maintaining an approachable configuration.
## Highlights
- Runs the official `photoprism/photoprism` image with non-root defaults (UID/GID 1000).
- Ships with SQLite out of the box; switch to MariaDB/MySQL by filling in the database section.
- Creates lightweight PVCs for `/photoprism/storage` (5GiB) and, optionally, `/photoprism/originals` (10GiB) the clusters default storage class is used unless you override it.
- Surfaces the most relevant customization settings (locale, themes, login footer, CORS/CDN, backup schedule) directly in Ranchers UI or via `values.yaml`.
## Quick Start
```bash
helm repo add photoprism https://charts.photoprism.app/photoprism
helm repo update photoprism
helm upgrade --install photos photoprism/photoprism-plus \
--namespace photos --create-namespace
```
This deploys PhotoPrism with SQLite storage. To use MariaDB (recommended for larger libraries):
```bash
helm upgrade --install photos photoprism/photoprism-plus \
--namespace photos \
--set database.driver=mysql \
--set database.server=mariadb.default.svc.cluster.local:3306 \
--set database.name=photoprism \
--set database.user=photoprism \
--set database.password=changeme
```
## Storage & Backups
- `persistence.storage` is always created and holds the application state.
- `persistence.originals` can be disabled or redirected to an NFS export when you manage originals elsewhere.
- Backup options (`PHOTOPRISM_BACKUP_*`) are exposed so you can point scheduled backups to another path or tweak retention.
## Customization
Key values you might want to adjust:
- `config.PHOTOPRISM_SITE_TITLE`, `config.PHOTOPRISM_APP_NAME`, `config.PHOTOPRISM_DEFAULT_THEME`
- `config.PHOTOPRISM_AUTH_MODE`, `config.PHOTOPRISM_LOGIN_INFO`, `config.PHOTOPRISM_PASSWORD_LENGTH`
- `config.PHOTOPRISM_CDN_URL`, `config.PHOTOPRISM_CORS_ORIGIN`
- `config.PHOTOPRISM_FILES_QUOTA`, `config.PHOTOPRISM_UPLOAD_LIMIT`
See [`values.yaml`](https://github.com/photoprism/photoprism/blob/develop/setup/charts/plus/values.yaml) for the full list.
## Networking & TLS
The chart exposes PhotoPrism on TCP 2342 through a ClusterIP service. You can override the service type or enable an Ingress resource when you terminate TLS in the cluster edge:
```yaml
service:
type: ClusterIP
port: 2342
ingress:
enabled: true
className: traefik
hosts:
- host: photos.example.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- photos.example.com
secretName: photos-tls
```
Because TLS typically terminates at the ingress or proxy layer, the chart keeps `PHOTOPRISM_DISABLE_TLS` set to `true`. Only enable PhotoPrisms internal TLS if your cluster design requires end-to-end encryption and you manage the certificates yourself.
## Security Tips
- When `adminPassword` is left blank, a random password is generated and stored in `secret/<release>-photoprism-secrets`.
- Prefer MariaDB/MySQL for multi-user setups or large libraries, and back up both the database and storage PVCs regularly.
- If you expose PhotoPrism on the public internet, pair it with HTTPS termination (Ingress/TLS or an external proxy) and keep the container image up to date.
## Getting Support
Commercial support is available with our Starter, Business, and Enterprise team plans:
- https://www.photoprism.app/teams#compare
- https://www.photoprism.app/kb/getting-support
## PhotoPrism® Documentation
For more information on specific features, services and related resources, please refer to the other documentation available in our Knowledge Base and User Guide:
- [PhotoPrism® User Guide](https://docs.photoprism.app/user-guide/)
- [PhotoPrism® Knowledge Base](https://www.photoprism.app/kb)

View file

@ -0,0 +1,413 @@
questions:
- variable: image.tag
group: "Customization"
label: "Image Tag"
description: "Override the container tag; leave empty to use the chart appVersion."
type: string
- variable: config.PHOTOPRISM_SITE_TITLE
group: "Customization"
label: "Site Title"
description: "Displayed title in the PhotoPrism UI. Example: Photo Library."
type: string
- variable: config.PHOTOPRISM_SITE_CAPTION
group: "Customization"
label: "Site Caption"
description: "Optional tagline shown beneath the title. Example: Explore Your Media."
type: string
- variable: config.PHOTOPRISM_SITE_DESCRIPTION
group: "Customization"
label: "Site Description"
description: "Short description used in metadata and link previews. Example: Private family archive."
type: string
- variable: config.PHOTOPRISM_SITE_AUTHOR
group: "Customization"
label: "Site Author"
description: "Owner or copyright notice displayed in metadata. Example: © 2025 The Smith Family."
type: string
- variable: config.PHOTOPRISM_SITE_FAVICON
group: "Customization"
label: "Site Favicon"
description: "Filename or URL of the favicon asset. Example: /photoprism/assets/favicon.ico."
type: string
- variable: config.PHOTOPRISM_SITE_PREVIEW
group: "Customization"
label: "Sharing Preview URL"
description: "Image URL used for social previews. Example: https://example.com/preview.jpg."
type: string
- variable: config.PHOTOPRISM_CDN_URL
group: "Customization"
label: "CDN URL"
description: "Base URL for serving static assets via CDN. Example: https://cdn.example.com."
type: string
- variable: config.PHOTOPRISM_CDN_VIDEO
group: "Customization"
label: "CDN Video URL"
description: "Optional CDN endpoint for streaming videos. Example: https://videos.example.com."
type: string
- variable: config.PHOTOPRISM_CORS_ORIGIN
group: "Customization"
label: "CORS Allowed Origin"
description: "Origin allowed to issue cross-origin requests. Example: https://app.example.com."
type: string
- variable: config.PHOTOPRISM_CORS_HEADERS
group: "Customization"
label: "CORS Headers"
description: "Comma-separated headers exposed for cross-origin requests."
type: string
default: "Accept, Accept-Ranges, Content-Disposition, Content-Encoding, Content-Range, Location"
- variable: config.PHOTOPRISM_CORS_METHODS
group: "Customization"
label: "CORS Methods"
description: "Allowed HTTP methods for cross-origin requests."
type: string
default: "GET, HEAD, OPTIONS"
- variable: config.PHOTOPRISM_APP_NAME
group: "Customization"
label: "App Name"
description: "Display name when PhotoPrism is installed as a PWA."
type: string
- variable: config.PHOTOPRISM_APP_ICON
group: "Customization"
label: "App Icon"
description: "Icon style for the PWA shortcut (logo, app, mint, square, etc.). Example: logo."
type: string
- variable: config.PHOTOPRISM_APP_COLOR
group: "Customization"
label: "App Color"
description: "Hex colour used for the PWA splash screen and tint. Example: #1d1d1e."
type: string
default: "#1d1d1e"
- variable: config.PHOTOPRISM_DEFAULT_TIMEZONE
group: "Customization"
label: "Default Timezone"
description: "Timezone name used for scheduling (e.g. Europe/Berlin)."
type: string
- variable: config.PHOTOPRISM_PLACES_LOCALE
group: "Customization"
label: "Places Locale"
description: "Language code for location details (en, de, local, etc.)."
type: string
default: "local"
- variable: config.PHOTOPRISM_DEFAULT_THEME
group: "Customization"
label: "Default Theme"
description: "Theme name applied to new users (leave blank for PhotoPrism default)."
type: string
- variable: config.PHOTOPRISM_DEFAULT_LOCALE
group: "Customization"
label: "Default Locale"
description: "Language code assigned to new users. Example: en or de."
type: string
default: "en"
- variable: config.PHOTOPRISM_AUTH_MODE
group: "Authentication"
label: "Authentication Mode"
description: "Select password-protected or public access."
type: enum
options:
- "password"
- "public"
default: "password"
- variable: config.PHOTOPRISM_LOG_LEVEL
group: "Customization"
label: "Log Level"
description: "Application log verbosity. Example: info."
type: enum
options:
- "trace"
- "debug"
- "info"
- "warning"
- "error"
default: "info"
- variable: adminUser
group: "Administration"
label: "Admin Username"
description: "Initial administrator account."
type: string
default: "admin"
required: true
- variable: adminPassword
group: "Administration"
label: "Admin Password"
description: "Leave empty to auto-generate and store the password in a secret."
type: string
- variable: config.PHOTOPRISM_PASSWORD_LENGTH
group: "Authentication"
label: "Minimum Password Length"
description: "Enforce a minimum password length for local users."
type: int
default: "8"
- variable: config.PHOTOPRISM_SESSION_MAXAGE
group: "Authentication"
label: "Session Max Age (Seconds)"
description: "Maximum session lifetime; doubled when 2FA is enabled."
type: int
default: "1209600"
- variable: config.PHOTOPRISM_SESSION_TIMEOUT
group: "Authentication"
label: "Session Idle Timeout (Seconds)"
description: "Maximum idle time before users are signed out."
type: int
default: "604800"
- variable: config.PHOTOPRISM_SESSION_CACHE
group: "Authentication"
label: "Session Cache TTL (Seconds)"
description: "Duration session data is cached in memory (60-3600)."
type: int
default: "900"
- variable: config.PHOTOPRISM_LOGIN_INFO
group: "Authentication"
label: "Login Footer Text"
description: "Optional message displayed below the login form."
type: string
- variable: config.PHOTOPRISM_LEGAL_INFO
group: "Customization"
label: "Legal Notice Text"
description: "Text displayed in the footer for legal notices."
type: string
- variable: config.PHOTOPRISM_LEGAL_URL
group: "Customization"
label: "Legal Notice URL"
description: "Link to full legal information."
type: string
- variable: config.PHOTOPRISM_WALLPAPER_URI
group: "Customization"
label: "Wallpaper URI"
description: "Background image URL for the login screen."
type: string
- variable: oidc.PHOTOPRISM_DISABLE_OIDC
group: "Authentication"
label: "Disable OpenID Connect"
description: "Set to false to enable OpenID Connect login with the settings below."
type: boolean
default: true
show_subquestion_if: false
subquestions:
- variable: oidc.PHOTOPRISM_OIDC_URI
label: "OIDC Issuer URI"
description: "Identity provider issuer URL."
type: string
- variable: oidc.PHOTOPRISM_OIDC_CLIENT
label: "OIDC Client ID"
description: "Client identifier issued by the identity provider. Example: photoprism-plus."
type: string
- variable: oidc.PHOTOPRISM_OIDC_SECRET
label: "OIDC Client Secret"
description: "Client secret stored in a Kubernetes Secret. Example: s3cr3t."
type: string
- variable: oidc.PHOTOPRISM_OIDC_SCOPES
label: "OIDC Scopes"
description: "Space-delimited scopes requested from the provider."
type: string
default: "openid email profile"
- variable: oidc.PHOTOPRISM_OIDC_REGISTER
label: "Auto Register Users"
description: "Automatically create PhotoPrism accounts after successful OIDC login."
type: boolean
default: true
- variable: oidc.PHOTOPRISM_OIDC_REDIRECT
label: "Force OIDC Redirect"
description: "Always redirect to the OIDC provider instead of showing the local login form."
type: boolean
default: false
- variable: oidc.PHOTOPRISM_OIDC_USERNAME
label: "OIDC Username Claim"
description: "Claim name used as the PhotoPrism username. Example: email."
type: string
default: "email"
- variable: oidc.PHOTOPRISM_OIDC_PROVIDER
label: "OIDC Provider Name"
description: "Label displayed on the login button."
type: string
- variable: oidc.PHOTOPRISM_OIDC_ICON
label: "OIDC Provider Icon"
description: "Optional icon URL for the login button."
type: string
- variable: oidc.PHOTOPRISM_OIDC_WEBDAV
label: "Enable OIDC WebDAV"
description: "Allow OIDC users to authenticate against WebDAV endpoints."
type: boolean
default: false
- variable: database.driver
group: "Database"
label: "Driver"
description: "Set to mysql when connecting to MariaDB/MySQL."
type: enum
options:
- "sqlite"
- "mysql"
default: "sqlite"
- variable: database.server
group: "Database"
label: "Server"
description: "Host:port or socket path for the database server."
type: string
- variable: database.name
group: "Database"
label: "Database Name"
description: "Schema name when using MariaDB/MySQL."
type: string
- variable: database.user
group: "Database"
label: "Database Username"
description: "User account for the schema."
type: string
- variable: database.password
group: "Database"
label: "Database Password"
description: "Optional password stored in the chart-managed secret."
type: string
- variable: database.timeout
group: "Database"
label: "Connection Timeout (Seconds)"
description: "Timeout when establishing a database connection."
type: int
default: "15"
- variable: database.conns
group: "Database"
label: "Max Open Connections"
description: "Maximum number of open connections (0 lets PhotoPrism decide)."
type: int
default: "0"
- variable: database.connsIdle
group: "Database"
label: "Max Idle Connections"
description: "Maximum number of idle connections (0 lets PhotoPrism decide)."
type: int
default: "0"
- variable: persistence.storage.size
group: "Storage"
label: "Storage Volume Size"
description: "PVC size for /photoprism/storage (can be expanded later)."
type: string
default: "5Gi"
- variable: config.PHOTOPRISM_FILES_QUOTA
group: "Storage"
label: "Total File Quota (GB)"
description: "Limit the total size of indexed originals (0 for unlimited)."
type: int
default: "0"
- variable: config.PHOTOPRISM_ORIGINALS_LIMIT
group: "Storage"
label: "Maximum Original Size (MB)"
description: "Largest allowed original file size."
type: int
default: "5000"
- variable: config.PHOTOPRISM_UPLOAD_LIMIT
group: "Storage"
label: "Maximum Upload Size (MB)"
description: "Largest allowed upload size."
type: int
default: "5000"
- variable: config.PHOTOPRISM_UPLOAD_ALLOW
group: "Storage"
label: "Allowed Upload Types"
description: "Comma-separated list of permitted upload extensions."
type: string
default: "zip,jpeg,jpg,png,gif,bmp,avif,webp,heic,heif,tiff,pdf,ai,svg,xmp"
- variable: config.PHOTOPRISM_IMPORT_ALLOW
group: "Storage"
label: "Allowed Import Types"
description: "Comma-separated list of permitted import extensions."
type: string
default: "zip,jpeg,jpg,png,gif,bmp,avif,webp,heic,heif,tiff,pdf,ai,svg,xmp"
- variable: persistence.originals.enabled
group: "Storage"
label: "Enable Originals PVC"
description: "Provision a PVC for originals; disable when mounting NFS manually."
type: boolean
default: true
show_subquestion_if: true
subquestions:
- variable: persistence.originals.size
label: "Originals Volume Size"
description: "PVC size for /photoprism/originals (can be expanded later)."
type: string
default: "10Gi"
- variable: persistence.originals.nfs.enabled
label: "Mount Originals via NFS"
description: "Use an existing NFS export instead of a dynamically provisioned PVC."
type: boolean
default: false
show_subquestion_if: true
subquestions:
- variable: persistence.originals.nfs.server
label: "NFS Server"
description: "Hostname or IP of the NFS server. Example: nfs.internal.local."
type: string
required: true
- variable: persistence.originals.nfs.path
label: "NFS Export Path"
description: "Export path on the NFS server. Example: /exports/photos."
type: string
required: true
- variable: config.PHOTOPRISM_BACKUP_SCHEDULE
group: "Backup"
label: "Backup Schedule"
description: "Cron schedule or keyword (daily/weekly) controlling automated backups. Example: 0 3 * * *."
type: string
default: "daily"
- variable: config.PHOTOPRISM_BACKUP_RETAIN
group: "Backup"
label: "Backups to Keep"
description: "Number of backup archives to retain (-1 keeps all). Example: 3."
type: int
default: "3"
- variable: config.PHOTOPRISM_BACKUP_PATH
group: "Backup"
label: "Backup Path"
description: "Custom base directory for backups. Example: /photoprism/backups."
type: string
- variable: config.PHOTOPRISM_BACKUP_DATABASE
group: "Backup"
label: "Backup Database"
description: "Enable scheduled database backups."
type: boolean
default: true
- variable: config.PHOTOPRISM_BACKUP_ALBUMS
group: "Backup"
label: "Backup Albums"
description: "Include YAML album metadata in backups."
type: boolean
default: true
- variable: cluster.integration.enabled
group: "Cluster Integration"
label: "Enable Portal Integration"
description: "Set to true when a PhotoPrism Portal is available."
type: boolean
default: false
show_subquestion_if: true
subquestions:
- variable: cluster.integration.secretName
label: "Portal Secret Name"
description: "Secret containing PHOTOPRISM_CLUSTER_* values (defaults to photoprism-cluster-secrets)."
type: string
- variable: cluster.integration.domain
label: "Cluster Domain Override"
description: "Optional domain suffix when not using the shared secret."
type: string
- variable: cluster.integration.portalURL
label: "Portal URL"
description: "Portal base URL if no secret is provided."
type: string
- variable: cluster.integration.joinToken
label: "Join Token"
description: "Portal-issued token used to register this node. Example: node_abc123."
type: string
- variable: cluster.integration.uuid
label: "Cluster UUID"
description: "Unique cluster identifier provided by the portal."
type: string
- variable: cluster.integration.cidr
label: "Cluster CIDR"
description: "Allowed IP range for trusted cluster traffic. Example: 10.42.0.0/16."
type: string
- variable: cluster.integration.trustedProxy
label: "Trusted Proxy"
description: "Reverse proxy address forwarded in X-Forwarded-For headers."
type: string
- variable: cluster.integration.jwtScope
label: "JWT Scope"
description: "Scope names allowed for issued cluster tokens."
type: string

View file

@ -0,0 +1,36 @@
PhotoPrism® has been deployed as release "{{ .Release.Name }}" in namespace "{{ .Release.Namespace }}".
1. Retrieve the admin password (auto-generated on first install unless adminPassword was set):
kubectl --namespace {{ .Release.Namespace }} get secret \
{{ include "photoprism-plus.fullname" . }}-secrets \
-o jsonpath="{.data.PHOTOPRISM_ADMIN_PASSWORD}" | base64 -d ; echo
Sign in as "{{ .Values.adminUser }}" with that password.
2. Access PhotoPrism:
{{- if .Values.ingress.enabled }}
An Ingress was created. Once your controller assigns an address, browse to one of the configured hosts:
{{- range .Values.ingress.hosts }}
- http(s)://{{ .host }}{{ (index .paths 0).path }}
{{- else }}
- http(s)://{{ include "photoprism-plus.name" $ }}/
{{- end }}
{{- else if eq .Values.service.type "LoadBalancer" }}
The service is type LoadBalancer; once an external IP is assigned, browse to it on port {{ .Values.service.port }}:
kubectl --namespace {{ .Release.Namespace }} get svc {{ include "photoprism-plus.fullname" . }}
{{- else }}
Forward the service port to your workstation:
kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ include "photoprism-plus.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }}
Then open http://localhost:{{ .Values.service.port }}/ in your browser.
{{- end }}
3. Documentation:
- User Guide: https://docs.photoprism.app/user-guide/
- Knowledge Base: https://www.photoprism.app/kb
- Support: https://www.photoprism.app/kb/getting-support

View file

@ -0,0 +1,63 @@
{{/*
General helpers for the PhotoPrism Plus chart
*/}}
{{- define "photoprism-plus.name" -}}
{{- if .Values.nameOverride -}}
{{- .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- "photoprism" -}}
{{- end -}}
{{- end -}}
{{- define "photoprism-plus.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-photoprism" .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- define "photoprism-plus.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version -}}
{{- end -}}
{{- define "photoprism-plus.labels" -}}
helm.sh/chart: {{ include "photoprism-plus.chart" . }}
app.kubernetes.io/name: {{ include "photoprism-plus.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{- define "photoprism-plus.clusterSecretName" -}}
{{- if .Values.cluster.integration.secretName -}}
{{- .Values.cluster.integration.secretName -}}
{{- else -}}
photoprism-cluster-secrets
{{- end -}}
{{- end -}}
{{- define "photoprism-plus.clusterDomain" -}}
{{- $domain := default "" .Values.cluster.integration.domain | trim -}}
{{- if and (eq $domain "") .Values.cluster.integration.enabled -}}
{{- $secret := lookup "v1" "Secret" .Release.Namespace (include "photoprism-plus.clusterSecretName" .) -}}
{{- if $secret -}}
{{- $encoded := index $secret.data "PHOTOPRISM_CLUSTER_DOMAIN" -}}
{{- if $encoded -}}
{{- $domain = b64dec $encoded | trim -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $domain -}}
{{- end -}}
{{- define "photoprism-plus.siteURL" -}}
{{- $site := default "" .Values.config.PHOTOPRISM_SITE_URL | trim -}}
{{- if and (eq $site "") .Values.cluster.integration.enabled -}}
{{- $domain := include "photoprism-plus.clusterDomain" . | trim -}}
{{- if ne $domain "" -}}
{{- printf "https://%s.%s/" .Release.Name $domain -}}
{{- end -}}
{{- else -}}
{{- $site -}}
{{- end -}}
{{- end -}}

View file

@ -0,0 +1,54 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "photoprism-plus.fullname" . }}
labels:
{{- include "photoprism-plus.labels" . | nindent 4 }}
{{- if .Values.ingress.annotations }}
annotations:
{{- toYaml .Values.ingress.annotations | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
rules:
{{- if not .Values.ingress.hosts }}
- host: {{ printf "%s" (include "photoprism-plus.name" .) | quote }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ include "photoprism-plus.fullname" . }}
port:
number: {{ .Values.service.port }}
{{- else }}
{{- range .Values.ingress.hosts }}
- host: {{ tpl .host $ | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType | default "Prefix" }}
backend:
service:
name: {{ include "photoprism-plus.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ tpl . $ | quote }}
{{- end }}
{{- if .secretName }}
secretName: {{ tpl .secretName $ | quote }}
{{- end }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,17 @@
{{- $orig := .Values.persistence.originals -}}
{{- if and $orig.enabled (not $orig.nfs.enabled) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "photoprism-plus.fullname" . }}-originals
labels:
{{- include "photoprism-plus.labels" . | nindent 4 }}
spec:
accessModes: {{ toYaml $orig.accessModes | nindent 4 }}
resources:
requests:
storage: {{ $orig.size }}
{{- if .Values.persistence.storageClassName }}
storageClassName: {{ .Values.persistence.storageClassName }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,17 @@
{{- $store := .Values.persistence.storage -}}
{{- if $store.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "photoprism-plus.fullname" . }}-storage
labels:
{{- include "photoprism-plus.labels" . | nindent 4 }}
spec:
accessModes: {{ toYaml $store.accessModes | nindent 4 }}
resources:
requests:
storage: {{ $store.size }}
{{- if .Values.persistence.storageClassName }}
storageClassName: {{ .Values.persistence.storageClassName }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,37 @@
{{- /*
Recover existing passwords via lookup so upgrades keep the same values.
On first install, a random admin password is generated. Note that lookup
returns nil under `helm template` and `helm install --dry-run`, so those
invocations always print a fresh random — cosmetic only; real installs
preserve the stored value.
*/ -}}
{{- $secretName := printf "%s-secrets" (include "photoprism-plus.fullname" .) -}}
{{- $existing := lookup "v1" "Secret" .Release.Namespace $secretName -}}
{{- $existingAdmin := "" -}}
{{- $existingDb := "" -}}
{{- if $existing -}}
{{- $existingAdmin = default "" (index $existing.data "PHOTOPRISM_ADMIN_PASSWORD") | b64dec | trim -}}
{{- $existingDb = default "" (index $existing.data "PHOTOPRISM_DATABASE_PASSWORD") | b64dec | trim -}}
{{- end -}}
{{- $adminPassword := default "" (.Values.adminPassword | toString | trim) -}}
{{- if eq $adminPassword "" -}}
{{- if ne $existingAdmin "" -}}
{{- $adminPassword = $existingAdmin -}}
{{- else -}}
{{- $adminPassword = randAlphaNum 16 -}}
{{- end -}}
{{- end -}}
{{- $dbPassword := default "" (.Values.database.password | toString | trim) -}}
{{- if and (eq $dbPassword "") (ne $existingDb "") -}}
{{- $dbPassword = $existingDb -}}
{{- end -}}
apiVersion: v1
kind: Secret
metadata:
name: {{ $secretName }}
type: Opaque
stringData:
PHOTOPRISM_ADMIN_PASSWORD: {{ $adminPassword | quote }}
{{- if ne $dbPassword "" }}
PHOTOPRISM_DATABASE_PASSWORD: {{ $dbPassword | quote }}
{{- end }}

View file

@ -0,0 +1,20 @@
{{- if hasKey .Values.service "enable" -}}
{{- fail "service.enable has been renamed to service.enabled — please update your values overrides." -}}
{{- end -}}
{{- if .Values.service.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "photoprism-plus.fullname" . }}
labels:
{{- include "photoprism-plus.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
selector:
app.kubernetes.io/name: {{ include "photoprism-plus.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: 2342
{{- end }}

View file

@ -0,0 +1,154 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "photoprism-plus.fullname" . }}
labels:
{{- include "photoprism-plus.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app.kubernetes.io/name: {{ include "photoprism-plus.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
serviceName: {{ include "photoprism-plus.fullname" . }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ include "photoprism-plus.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
{{- with .Values.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- with .Values.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- if .Values.command }}
command:
{{- range .Values.command }}
- {{ . | quote }}
{{- end }}
{{- end }}
{{- if .Values.args }}
args:
{{- range .Values.args }}
- {{ . | quote }}
{{- end }}
{{- end }}
env:
{{- $siteURL := include "photoprism-plus.siteURL" . | trim -}}
{{- if ne $siteURL "" }}
- name: PHOTOPRISM_SITE_URL
value: {{ $siteURL | quote }}
{{- end }}
{{- if .Values.cluster.integration.enabled }}
{{- $integration := .Values.cluster.integration -}}
{{- $secretName := include "photoprism-plus.clusterSecretName" . | trim -}}
{{- $clusterFields := dict
"PHOTOPRISM_PORTAL_URL" $integration.portalURL
"PHOTOPRISM_CLUSTER_DOMAIN" $integration.domain
"PHOTOPRISM_CLUSTER_UUID" $integration.uuid
"PHOTOPRISM_CLUSTER_CIDR" $integration.cidr
"PHOTOPRISM_TRUSTED_PROXY" $integration.trustedProxy
"PHOTOPRISM_JOIN_TOKEN" $integration.joinToken
"PHOTOPRISM_JWT_SCOPE" $integration.jwtScope -}}
{{- range $key, $value := $clusterFields }}
{{- $inline := trim (default "" $value) -}}
{{- if ne $inline "" }}
- name: {{ $key }}
value: {{ $inline | quote }}
{{- else if ne $secretName "" }}
- name: {{ $key }}
valueFrom:
secretKeyRef:
name: {{ $secretName | quote }}
key: {{ $key }}
optional: true
{{- end }}
{{- end }}
{{- end }}
- name: PHOTOPRISM_ADMIN_USER
value: {{ .Values.adminUser | quote }}
- name: PHOTOPRISM_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: {{ printf "%s-secrets" (include "photoprism-plus.fullname" .) }}
key: PHOTOPRISM_ADMIN_PASSWORD
- name: PHOTOPRISM_DATABASE_DRIVER
value: {{ .Values.database.driver | quote }}
{{- $dbServer := trim (default "" .Values.database.server) -}}
{{- if ne $dbServer "" }}
- name: PHOTOPRISM_DATABASE_SERVER
value: {{ $dbServer | quote }}
{{- end }}
{{- $dbName := trim (default "" .Values.database.name) -}}
{{- if ne $dbName "" }}
- name: PHOTOPRISM_DATABASE_NAME
value: {{ $dbName | quote }}
{{- end }}
{{- $dbUser := trim (default "" .Values.database.user) -}}
{{- if ne $dbUser "" }}
- name: PHOTOPRISM_DATABASE_USER
value: {{ $dbUser | quote }}
{{- end }}
{{- $dbPassword := trim (default "" .Values.database.password) -}}
{{- if ne $dbPassword "" }}
- name: PHOTOPRISM_DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: {{ printf "%s-secrets" (include "photoprism-plus.fullname" .) }}
key: PHOTOPRISM_DATABASE_PASSWORD
{{- end }}
- name: PHOTOPRISM_DATABASE_TIMEOUT
value: {{ printf "%v" .Values.database.timeout | quote }}
- name: PHOTOPRISM_DATABASE_CONNS
value: {{ printf "%v" .Values.database.conns | quote }}
- name: PHOTOPRISM_DATABASE_CONNS_IDLE
value: {{ printf "%v" .Values.database.connsIdle | quote }}
{{- range $key, $value := .Values.oidc }}
{{- if ne (printf "%v" $value) "" }}
- name: {{ $key }}
value: {{ printf "%v" $value | quote }}
{{- end }}
{{- end }}
{{- range $key, $value := .Values.config }}
{{- if ne (printf "%v" $value) "" }}
- name: {{ $key }}
value: {{ printf "%v" $value | quote }}
{{- end }}
{{- end }}
ports:
- containerPort: 2342
name: http
volumeMounts:
{{- if .Values.persistence.originals.enabled }}
- name: originals
mountPath: {{ .Values.persistence.originals.mountPath }}
{{- end }}
{{- if .Values.persistence.storage.enabled }}
- name: storage
mountPath: {{ .Values.persistence.storage.mountPath }}
{{- end }}
volumes:
{{- if .Values.persistence.originals.enabled }}
- name: originals
{{- if and .Values.persistence.originals.enabled .Values.persistence.originals.nfs.enabled }}
nfs:
server: {{ required "persistence.originals.nfs.server is required when NFS mode is enabled" .Values.persistence.originals.nfs.server | quote }}
path: {{ required "persistence.originals.nfs.path is required when NFS mode is enabled" .Values.persistence.originals.nfs.path | quote }}
{{- else }}
persistentVolumeClaim:
claimName: {{ include "photoprism-plus.fullname" . }}-originals
{{- end }}
{{- end }}
{{- if .Values.persistence.storage.enabled }}
- name: storage
persistentVolumeClaim:
claimName: {{ include "photoprism-plus.fullname" . }}-storage
{{- end }}

View file

@ -0,0 +1,160 @@
# Default values for the PhotoPrism® Helm chart.
# These defaults are tuned for home labs and small teams; enable the optional
# portal integration block only if a PhotoPrism Portal is available in your cluster.
replicaCount: 1
image:
repository: docker.io/photoprism/photoprism
tag: ""
pullPolicy: IfNotPresent
command: []
args: []
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
runAsUser: 1000
runAsGroup: 1000
podSecurityContext:
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
nameOverride: ""
fullnameOverride: ""
adminUser: "admin"
adminPassword: ""
cluster:
integration:
enabled: false
secretName: ""
domain: ""
portalURL: ""
uuid: ""
cidr: ""
trustedProxy: ""
joinToken: ""
jwtScope: ""
persistence:
storageClassName: ""
originals:
enabled: true
size: 10Gi
accessModes:
- ReadWriteOnce
mountPath: /photoprism/originals
nfs:
enabled: false
server: ""
path: ""
storage:
enabled: true
size: 5Gi
accessModes:
- ReadWriteOnce
mountPath: /photoprism/storage
service:
enabled: true
type: ClusterIP
port: 2342
ingress:
enabled: false
className: ""
annotations: {}
hosts: []
tls: []
resources:
limits:
cpu: 4000m
memory: 6Gi
requests:
cpu: 500m
memory: 1Gi
nodeSelector: {}
tolerations: []
affinity: {}
oidc:
PHOTOPRISM_DISABLE_OIDC: true
PHOTOPRISM_OIDC_URI: ""
PHOTOPRISM_OIDC_CLIENT: ""
PHOTOPRISM_OIDC_SECRET: ""
PHOTOPRISM_OIDC_SCOPES: "openid email profile"
PHOTOPRISM_OIDC_REGISTER: true
PHOTOPRISM_OIDC_REDIRECT: false
PHOTOPRISM_OIDC_USERNAME: "email"
PHOTOPRISM_OIDC_PROVIDER: ""
PHOTOPRISM_OIDC_ICON: ""
PHOTOPRISM_OIDC_WEBDAV: false
config:
PHOTOPRISM_SITE_URL: ""
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: ""
PHOTOPRISM_SITE_DESCRIPTION: ""
PHOTOPRISM_SITE_AUTHOR: ""
PHOTOPRISM_SITE_FAVICON: ""
PHOTOPRISM_SITE_PREVIEW: ""
PHOTOPRISM_APP_NAME: "PhotoPrism"
PHOTOPRISM_APP_ICON: ""
PHOTOPRISM_APP_COLOR: "#1d1d1e"
PHOTOPRISM_DEFAULT_THEME: ""
PHOTOPRISM_DEFAULT_LOCALE: "en"
PHOTOPRISM_DEFAULT_TIMEZONE: ""
PHOTOPRISM_PLACES_LOCALE: "local"
PHOTOPRISM_AUTH_MODE: "password"
PHOTOPRISM_PASSWORD_LENGTH: 8
PHOTOPRISM_SESSION_MAXAGE: 1209600
PHOTOPRISM_SESSION_TIMEOUT: 604800
PHOTOPRISM_SESSION_CACHE: 900
PHOTOPRISM_LOGIN_INFO: ""
PHOTOPRISM_LOG_LEVEL: "info"
PHOTOPRISM_HTTP_COMPRESSION: "zstd,gzip"
PHOTOPRISM_CDN_URL: ""
PHOTOPRISM_CDN_VIDEO: ""
PHOTOPRISM_CORS_ORIGIN: ""
PHOTOPRISM_CORS_HEADERS: "Accept, Accept-Ranges, Content-Disposition, Content-Encoding, Content-Range, Location"
PHOTOPRISM_CORS_METHODS: "GET, HEAD, OPTIONS"
PHOTOPRISM_LEGAL_INFO: ""
PHOTOPRISM_LEGAL_URL: ""
PHOTOPRISM_BACKUP_SCHEDULE: "daily"
PHOTOPRISM_BACKUP_RETAIN: "3"
PHOTOPRISM_BACKUP_DATABASE: true
PHOTOPRISM_BACKUP_ALBUMS: true
PHOTOPRISM_BACKUP_PATH: ""
PHOTOPRISM_FILES_QUOTA: 0
PHOTOPRISM_ORIGINALS_LIMIT: 5000
PHOTOPRISM_UPLOAD_LIMIT: 5000
PHOTOPRISM_UPLOAD_ALLOW: "zip,jpeg,jpg,png,gif,bmp,avif,webp,heic,heif,tiff,pdf,ai,svg,xmp"
PHOTOPRISM_IMPORT_ALLOW: "zip,jpeg,jpg,png,gif,bmp,avif,webp,heic,heif,tiff,pdf,ai,svg,xmp"
PHOTOPRISM_UPLOAD_ARCHIVES: true
PHOTOPRISM_UPLOAD_NSFW: true
PHOTOPRISM_DISABLE_TLS: true
PHOTOPRISM_DISABLE_STS: true
PHOTOPRISM_DISABLE_MCP: false
PHOTOPRISM_WALLPAPER_URI: ""
# Database connection overrides (blank defaults keep PhotoPrism's internal sqlite)
database:
driver: "sqlite"
server: ""
name: "photoprism"
user: "photoprism"
password: ""
timeout: 15
conns: 0
connsIdle: 0

View file

@ -183,12 +183,15 @@ services:
OLLAMA_MULTIUSER_CACHE: "false" # optimize prompt caching for multi-user scenarios
OLLAMA_NOPRUNE: "false" # disables pruning of model blobs at startup
OLLAMA_NOHISTORY: "true" # disables readline history
OLLAMA_FLASH_ATTENTION: "false" # enables the experimental flash attention feature
OLLAMA_KV_CACHE_TYPE: "f16" # cache quantization (f16, q8_0, or q4_0)
OLLAMA_FLASH_ATTENTION: "true" # required for OLLAMA_KV_CACHE_TYPE quantization
OLLAMA_KV_CACHE_TYPE: "f16" # cache precision: f16 (default), q8_0, q4_0
OLLAMA_SCHED_SPREAD: "false" # allows scheduling models across all GPUs.
OLLAMA_NEW_ENGINE: "true" # enables the new Ollama engine
# OLLAMA_DEBUG: "true" # shows additional debug information
# OLLAMA_INTEL_GPU: "true" # enables experimental Intel GPU detection
## Telemetry / privacy opt-outs (containers do not inherit /etc/environment):
DO_NOT_TRACK: "true"
HF_HUB_DISABLE_TELEMETRY: "1"
# OLLAMA_NO_CLOUD: "1" # uncomment to disable Ollama Cloud models/features
volumes:
- "./ollama:/root/.ollama"

View file

@ -188,12 +188,15 @@ services:
OLLAMA_MULTIUSER_CACHE: "false" # optimize prompt caching for multi-user scenarios
OLLAMA_NOPRUNE: "false" # disables pruning of model blobs at startup
OLLAMA_NOHISTORY: "true" # disables readline history
OLLAMA_FLASH_ATTENTION: "false" # enables the experimental flash attention feature
OLLAMA_KV_CACHE_TYPE: "f16" # cache quantization (f16, q8_0, or q4_0)
OLLAMA_FLASH_ATTENTION: "true" # required for OLLAMA_KV_CACHE_TYPE quantization
OLLAMA_KV_CACHE_TYPE: "f16" # cache precision: f16 (default), q8_0, q4_0
OLLAMA_SCHED_SPREAD: "false" # allows scheduling models across all GPUs.
OLLAMA_NEW_ENGINE: "true" # enables the new Ollama engine
# OLLAMA_DEBUG: "true" # shows additional debug information
# OLLAMA_INTEL_GPU: "true" # enables experimental Intel GPU detection
## Telemetry / privacy opt-outs (containers do not inherit /etc/environment):
DO_NOT_TRACK: "true"
HF_HUB_DISABLE_TELEMETRY: "1"
# OLLAMA_NO_CLOUD: "1" # uncomment to disable Ollama Cloud models/features
## NVIDIA GPU Hardware Acceleration (see https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html):
# NVIDIA_VISIBLE_DEVICES: "all"
# NVIDIA_DRIVER_CAPABILITIES: "compute,utility"

View file

@ -188,12 +188,15 @@ services:
OLLAMA_MULTIUSER_CACHE: "false" # optimize prompt caching for multi-user scenarios
OLLAMA_NOPRUNE: "false" # disables pruning of model blobs at startup
OLLAMA_NOHISTORY: "true" # disables readline history
OLLAMA_FLASH_ATTENTION: "false" # enables the experimental flash attention feature
OLLAMA_KV_CACHE_TYPE: "f16" # cache quantization (f16, q8_0, or q4_0)
OLLAMA_FLASH_ATTENTION: "true" # required for OLLAMA_KV_CACHE_TYPE quantization
OLLAMA_KV_CACHE_TYPE: "f16" # cache precision: f16 (default), q8_0, q4_0
OLLAMA_SCHED_SPREAD: "false" # allows scheduling models across all GPUs.
OLLAMA_NEW_ENGINE: "true" # enables the new Ollama engine
# OLLAMA_DEBUG: "true" # shows additional debug information
# OLLAMA_INTEL_GPU: "true" # enables experimental Intel GPU detection
## Telemetry / privacy opt-outs (containers do not inherit /etc/environment):
DO_NOT_TRACK: "true"
HF_HUB_DISABLE_TELEMETRY: "1"
# OLLAMA_NO_CLOUD: "1" # uncomment to disable Ollama Cloud models/features
## NVIDIA GPU Hardware Acceleration (see https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html):
NVIDIA_VISIBLE_DEVICES: "all"
NVIDIA_DRIVER_CAPABILITIES: "compute,utility"