This guide describes how to run a PhotoPrism® Portal and its tenant instances on a Kubernetes cluster. The Portal terminates the shared domain, authenticates users as the cluster’s OpenID Provider, and provisions a database for each tenant.
A public Helm chart for the Portal is not available yet. The examples below use plain Kubernetes manifests for the Portal and the public photoprism-pro chart for tenants. If you need an orchestrated Portal chart for your platform, contact us.
Overview
A cluster has one Portal node and one or more instance (tenant) nodes:
- The Portal runs the
photoprism/portalimage and is exposed over HTTPS at the shared domain. - Tenant instances run the
photoprism/proimage and register with the Portal on first boot. - The Portal and tenants share one database server; the Portal provisions a database and user per tenant.
See Config Options for every variable referenced below, and Architecture for the cluster model.
1. Namespace & Cluster Secret
Create a namespace and a secret with the shared join token and database credentials. Keeping these in a Secret avoids hard-coding them into the Deployment.
kubectl create namespace photoprism
kubectl -n photoprism create secret generic portal-secrets \
--from-literal=PHOTOPRISM_JOIN_TOKEN='<a-strong-shared-secret>' \
--from-literal=PHOTOPRISM_ADMIN_PASSWORD='<portal-admin-password>' \
--from-literal=PHOTOPRISM_DATABASE_PASSWORD='<db-password>' \
--from-literal=PHOTOPRISM_DATABASE_PROVISION_DSN='root:<root-password>@tcp(mariadb:3306)/'
The join token must be at least 24 characters long. The provisioning DSN is the privileged account the Portal uses to create per-tenant databases; it is kept on the Portal only.
2. Deploy the Portal
The Portal needs a persistent volume for its application storage, a Service, and ingress that terminates TLS at the shared domain:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: portal-storage
namespace: photoprism
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 15Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: portal
namespace: photoprism
spec:
replicas: 1
selector:
matchLabels: { app: portal }
template:
metadata:
labels: { app: portal }
spec:
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: portal
image: photoprism/portal:latest
ports:
- containerPort: 2342
env:
- { name: PHOTOPRISM_NODE_ROLE, value: "portal" }
- { name: PHOTOPRISM_CLUSTER_DOMAIN, value: "portal.example.com" }
- { name: PHOTOPRISM_SITE_URL, value: "https://portal.example.com/" }
- { name: PHOTOPRISM_PORTAL_PROXY, value: "true" }
- { name: PHOTOPRISM_PORTAL_PROXY_URI, value: "/i/" }
- { name: PHOTOPRISM_DATABASE_DRIVER, value: "mysql" }
- { name: PHOTOPRISM_DATABASE_SERVER, value: "mariadb:3306" }
- { name: PHOTOPRISM_DATABASE_NAME, value: "photoprism_portal" }
- { name: PHOTOPRISM_DATABASE_USER, value: "portal" }
envFrom:
- secretRef: { name: portal-secrets }
volumeMounts:
- { name: storage, mountPath: /photoprism/storage }
volumes:
- name: storage
persistentVolumeClaim: { claimName: portal-storage }
---
apiVersion: v1
kind: Service
metadata:
name: portal
namespace: photoprism
spec:
selector: { app: portal }
ports:
- port: 2342
targetPort: 2342
The Portal’s own database (PHOTOPRISM_DATABASE_NAME, here photoprism_portal) must already exist before the Portal starts. On first start, the Portal creates a superadmin account and is ready to register instances.
3. Shared-Domain Ingress
Expose the Portal over HTTPS at the shared domain. Because the Portal proxies tenants under /i/<name>/, a single hostname and certificate cover the whole cluster:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: portal
namespace: photoprism
spec:
rules:
- host: portal.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: portal
port: { number: 2342 }
tls:
- hosts: ["portal.example.com"]
secretName: portal-tls
PhotoPrism keeps internal TLS disabled because TLS terminates at the ingress. For separate-hostname layouts (one host and certificate per instance) instead of shared-domain routing, see Portal & Clusters.
4. Join Tenant Instances
Tenants run a standard PhotoPrism instance configured to join the cluster. The public photoprism-pro chart registers an instance with a Portal when cluster integration is enabled:
helm repo add photoprism https://charts.photoprism.app/photoprism
helm repo update photoprism
helm upgrade --install media photoprism/photoprism-pro \
--namespace photoprism \
--set cluster.integration.enabled=true \
--set cluster.integration.domain=portal.example.com \
--set cluster.integration.portalURL=http://portal.photoprism.svc.cluster.local:2342/ \
--set cluster.integration.joinToken=<the-same-shared-secret> \
--set config.PHOTOPRISM_NODE_NAME=media \
--set config.PHOTOPRISM_SITE_URL=https://portal.example.com/i/media/
On first start the instance registers with the Portal, receives its database credentials, and becomes reachable at https://portal.example.com/i/media/. Registration is idempotent and safe to repeat on every restart. Leave the tenant PHOTOPRISM_OIDC_URI, _CLIENT, and _SECRET empty — cluster OIDC derives them from the node credentials (see Config Options).
5. Storage
- The Portal stores its application state on a writable
PersistentVolumeClaimmounted at/photoprism/storage. - Each tenant needs its own storage PVC and, if it indexes a shared library, an originals volume (a PVC or an NFS-backed claim).
- Use your cluster’s default
StorageClassunless a workload requires a specific one. Size the database StatefulSet PVC for the combined tenant schemas.
Firewall & Networking
For the cluster-internal ports, outgoing-connection allowlist, and kernel-module requirements that apply to any PhotoPrism Kubernetes deployment, see Getting Started with Rancher and Kubernetes. For OpenShift specifics (Routes, SCC, Form View), see OpenShift.
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: