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/portal image and is exposed over HTTPS at the shared domain.
  • Tenant instances run the photoprism/pro image 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 account the Portal uses to create per-tenant databases; it is kept on the Portal only. The root account is fine for a test database; for production, create a dedicated account as described in Database Provisioning.

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 database.driver=mysql \
  --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 PersistentVolumeClaim mounted 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 StorageClass unless a workload requires a specific one. Size the database StatefulSet PVC for the combined tenant schemas.

Firewall & Networking

Set PHOTOPRISM_CLUSTER_CIDR on the Portal to your cluster’s pod network, for example 10.42.0.0/16 on k3s; instances outside it cannot register. Several ranges are separated by commas. See Config Options for more examples and how to look up the range. The Portal reads its environment only when it starts, so restart the Portal Deployment after changing a value in a Secret (a change to the Deployment’s own env triggers a rollout by itself), for example with kubectl -n photoprism rollout restart deployment/portal, and then restart the instances.

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: