Skip to main content

PhotoPrism on GKE Autopilot

PhotoPrism on GKE Autopilot

PhotoPrism is a self-hosted, AI-powered photo and video management application: it browses, organizes, and shares a personal media library with automatic tagging, facial recognition, and full-text/visual search, all served from a single Go binary with an embedded SQLite database. This module deploys PhotoPrism on GKE Autopilot on top of the App_GKE foundation, which provisions and manages the shared Google Cloud and Kubernetes infrastructure.

This guide focuses on the cloud services PhotoPrism uses and how to explore and operate them from the Google Cloud Console and the command line. For the mechanics that are common to every GKE application — Workload Identity, ingress, autoscaling, CI/CD, Cloud Armor, IAP, Binary Authorization, VPC Service Controls, backups, and the deployment lifecycle — refer to the App_GKE foundation guide rather than repeating them here.


1. Overview

PhotoPrism runs as a single Go binary web workload, deployed as a StatefulSet with a block Persistent Volume Claim rather than a stateless Deployment. The deployment wires together a focused set of Google Cloud services:

CapabilityGoogle Cloud serviceNotes
ComputeGKE AutopilotPhotoPrism pod on port 2342, StatefulSet by default
DatabaseNoneEmbedded SQLite (PHOTOPRISM_DATABASE_DRIVER=sqlite) — no Cloud SQL instance is provisioned
Block storagePersistent Disk (block PVC)/photoprism (SQLite database, cache, originals, imports) — required, gcsfuse cannot safely back SQLite
Object storageCloud StorageA storage bucket is provisioned but only mounted via GCS FUSE if the block PVC is disabled
SecretsSecret ManagerAuto-generated admin password (PHOTOPRISM_ADMIN_PASSWORD)
IngressCloud Load BalancingExternal LoadBalancer with a reserved static IP; optional custom domain + managed certificate

Sensible defaults worth knowing up front:

  • No database is ever provisioned. PhotoPrism_Common hardcodes database_type = "NONE" and enable_cloudsql_volume = false; PhotoPrism manages its own SQLite files under /photoprism/storage. The GKE-level database_type/application_database_*/enable_mysql_plugins/etc. variables are all inert placeholders forwarded for foundation compatibility only.
  • Block PVC, not gcsfuse, is mandatory. stateful_pvc_enabled = true by default, resolving workload_type to StatefulSet and mounting a standard-rwo (SSD) 20Gi Persistent Disk at /photoprism. gcsfuse cannot safely host SQLite or the media index, so the module automatically sets enable_gcs_storage_volume = false on the Common layer when the PVC is enabled, avoiding a double-mount at the same path.
  • Single replica, always. min_instance_count = 1, max_instance_count = 1. PhotoPrism serves one shared SQLite library from one writable volume — do not scale beyond 1.
  • Redis is forced off. The GKE-level enable_redis variable defaults true (the App_GKE foundation default), but PhotoPrism_GKE's main.tf hard-overrides it to false — PhotoPrism has no Redis integration, so no Memorystore/NFS Redis host is ever injected.
  • NFS is off by default (enable_nfs = false). The block PVC is the durable store; NFS is not needed unless you add custom jobs/services that require shared filesystem access.
  • Admin password is auto-generated. A 24-character password is created and stored in Secret Manager, then injected as the PHOTOPRISM_ADMIN_PASSWORD secret env var; the admin username is the plain admin_username variable (default admin).
  • Custom image build is a thin mirror, not app logic. The build wraps the upstream photoprism/photoprism image (FROM photoprism/photoprism:${PHOTOPRISM_VERSION}) so the Foundation can mirror it into Artifact Registry; the app-specific build arg is PHOTOPRISM_VERSION (not the generic APP_VERSION), pinned to 240915 when application_version = "latest".

2. Google Cloud Services & How to Explore Them

All commands assume you have run gcloud container clusters get-credentials <cluster> --region <region> --project <project> and that PROJECT, REGION, and NAMESPACE are set. The namespace and other identifiers are reported in the deployment Outputs.

A. GKE Autopilot — the PhotoPrism StatefulSet

PhotoPrism runs as a StatefulSet (not a Deployment) so its single pod gets a stable identity and an ordered restart, matched to its single writable block PVC. Autopilot bills for the CPU/memory the pod actually requests.

  • Console: Kubernetes Engine → Workloads → select the PhotoPrism StatefulSet for pods, revisions, and events. Kubernetes Engine → Services & Ingress shows the external IP.
  • CLI:
    kubectl get pods,svc,statefulset -n "$NAMESPACE"
    kubectl logs -n "$NAMESPACE" statefulset/<service-name> --tail=100
    kubectl describe pod -n "$NAMESPACE" -l app=<service-name>

See App_GKE for how Autopilot, scaling, and the workload type (Deployment vs StatefulSet) are managed.

B. Persistent block storage (Persistent Disk)

All PhotoPrism state — the SQLite database and cache (/photoprism/storage), imported media (/photoprism/originals), and staged imports (/photoprism/import) — lives on a single per-pod block PVC provisioned by the StatefulSet at /photoprism, backed by the standard-rwo StorageClass (Balanced Persistent Disk / SSD) by default, sized 20Gi.

  • Console: Kubernetes Engine → Storage → Persistent Volume Claims; Compute Engine → Disks.
  • CLI:
    kubectl get pvc,pv -n "$NAMESPACE"
    gcloud compute disks list --project "$PROJECT" --filter="name~<service-name>"

See App_GKE for StatefulSet PVC provisioning, StorageClass options, and the GKE SSD_TOTAL_GB quota caveat.

C. Cloud Storage

A dedicated Cloud Storage bucket (suffix storage) is provisioned by default (create_cloud_storage = true), but it is only mounted into the pod via GCS FUSE when the block PVC is disabled (stateful_pvc_enabled = false) — in the default StatefulSet configuration the bucket exists but is unused by the running container.

  • Console: Cloud Storage → Buckets.
  • CLI:
    gcloud storage buckets list --project "$PROJECT" --filter="name~storage"

See App_GKE for CMEK options and GCS Fuse CSI driver mounts.

D. Secret Manager

One secret is generated automatically: the PhotoPrism admin password (secret-<prefix>-photoprism-admin-password), injected into the container as PHOTOPRISM_ADMIN_PASSWORD. On GKE, secrets are projected into pods via the Secret Store CSI driver.

  • Console: Security → Secret Manager.
  • CLI:
    gcloud secrets list --project "$PROJECT" --filter="name~photoprism-admin-password"
    gcloud secrets versions access latest --secret=<admin-password-secret-name> --project "$PROJECT"

See App_GKE for the Secret Store CSI integration and rotation.

E. Networking & ingress

By default the workload is exposed through a ClusterIP Service (service_type = "ClusterIP"); switch to LoadBalancer for a direct external IP, or enable a custom domain (enable_custom_domain = true by default) via the Kubernetes Gateway API with a Google-managed certificate.

  • Console: Network services → Load balancing; VPC network → IP addresses.
  • CLI:
    kubectl get svc,gateway,httproute -n "$NAMESPACE"
    gcloud compute addresses list --project "$PROJECT"

See App_GKE for custom domains, Cloud CDN, and static IP details.

F. Cloud Logging & Monitoring

Pod stdout/stderr flow to Cloud Logging; GKE metrics flow to Cloud Monitoring. An optional uptime check (uptime_check_config, disabled by default) and custom alert policies are available.

  • Console: Logging → Logs Explorer; Monitoring → Dashboards / Alerting.
  • CLI:
    gcloud logging read 'resource.type="k8s_container" AND resource.labels.namespace_name="'"$NAMESPACE"'"' \
    --project "$PROJECT" --limit 50

3. PhotoPrism Application Behaviour

  • No init/db-create job. initialization_jobs defaults to [] — there is no database to bootstrap. PhotoPrism creates and migrates its own SQLite schema on first boot, under the mounted block PVC.
  • Storage layout. All state lives under the single mounted directory /photoprism: PHOTOPRISM_STORAGE_PATH=/photoprism/storage (SQLite database + cache), PHOTOPRISM_ORIGINALS_PATH=/photoprism/originals (imported/indexed media), PHOTOPRISM_IMPORT_PATH=/photoprism/import (staged imports).
  • Admin account. PHOTOPRISM_ADMIN_USER is the plain admin_username variable (default admin); PHOTOPRISM_ADMIN_PASSWORD is the auto-generated Secret Manager value, injected as a secret env var. PHOTOPRISM_AUTH_MODE = "password" is set explicitly. Retrieve the password from Secret Manager before first login.
  • Site URL. PHOTOPRISM_SITE_URL is empty by default — PhotoPrism tolerates this and falls back to the request host, but set site_url to the deployed URL for correctly generated absolute links.
  • fsGroup for the PVC. PhotoPrism runs as UID 1000 / GID 2000; the StatefulSet sets stateful_fs_group = 3000 (the upstream Helm chart's convention) so the mounted PVC is group-writable.
  • Health path. Both startup and liveness probes are HTTP GET /api/v1/status (initial delay 15s / 10 retries for startup, 30s / 3 retries for liveness) — no auth required. The Group-10 foundation-level startup_probe_config/health_check_config variables mirror the same path as generic defaults.
  • Scaling is pinned to one pod. min_instance_count = 1 and max_instance_count = 1 — PhotoPrism keeps a single writable SQLite database and a single writable media volume; there is no multi-writer support.
  • Inspect the running config and storage:
    kubectl get pods,pvc -n "$NAMESPACE"
    kubectl exec -n "$NAMESPACE" statefulset/<service-name> -- env | grep PHOTOPRISM_
    kubectl logs -n "$NAMESPACE" statefulset/<service-name> --tail=100

4. Configuration Variables

Variables are grouped exactly as they appear on the deployment platform. Only settings specific to or notable for PhotoPrism are listed; every other input is inherited from App_GKE with its standard behaviour and defaults.

Group 3 — Application Identity

VariableDefaultDescription
application_namephotoprismBase name for resources. Do not change after first deploy.
application_versionlatestphotoprism/photoprism image tag used as the custom-build base; latest is pinned to a known-good tag (240915) at build time.
admin_usernameadminInitial admin account username (PHOTOPRISM_ADMIN_USER); the password is generated separately.
site_url""Public site URL (PHOTOPRISM_SITE_URL). Empty falls back to the request host; set once the external URL is known for correct absolute links.

Group 4 — Runtime & Scaling

VariableDefaultDescription
cpu_limit1000mCPU allocated to the PhotoPrism container. Indexing/thumbnailing is CPU-bound; raise for large libraries.
memory_limit1GiMemory allocated to the container. <!-- TODO: verify --> PhotoPrism_Common's own default is 4Gi; PhotoPrism_GKE's variable default is 1Gi — raise for large libraries.
min_instance_count1Keep at 1 to avoid cold starts during index loading.
max_instance_count1Keep at 1 — single writable SQLite DB and media volume.
container_port2342Not forwarded to App_GKE; PhotoPrism always serves on 2342, fixed by PhotoPrism_Common.
enable_cloudsql_volumefalseNo Cloud SQL Auth Proxy sidecar — PhotoPrism has no external database.

Group 7 — StatefulSet (block PVC)

VariableDefaultDescription
stateful_pvc_enabledtrueRequired. gcsfuse cannot safely back SQLite/the media index; when true without an explicit workload_type, resolves to StatefulSet and disables the GCS FUSE storage volume automatically.
stateful_pvc_size20GiSize the PVC to hold all collections plus overhead.
stateful_pvc_mount_path/photoprismPhotoPrism's data dir — covers storage (SQLite/cache) and originals.
stateful_pvc_storage_classstandard-rwoBalanced PD (SSD) — draws the SSD_TOTAL_GB quota; see §6.
stateful_fs_group3000GID for pod-level fsGroup; PhotoPrism runs as UID 1000/GID 2000.
stateful_pod_management_policynullOrderedReadySafe, ordered restarts for a stateful single-pod workload.

Group 10 — Health & Observability

VariableDefaultDescription
startup_probe (Common)HTTP /api/v1/status, 15s delay, 10 retriesApplied to the PhotoPrism container.
liveness_probe (Common)HTTP /api/v1/status, 30s delay, 3 retriesApplied to the PhotoPrism container.
uptime_check_configenabled = false, path /api/v1/statusOptional external uptime check; off by default.

Group 14 — Cloud Storage

VariableDefaultDescription
create_cloud_storagetrueCreates the storage bucket even though it is unused when the block PVC (default) is active.
gcs_volumes[]Additional GCS FUSE mounts beyond the (auto-managed) PhotoPrism storage volume.

Group 15 — Redis

VariableDefaultDescription
enable_redistrue (var default) → forced falsemain.tf hard-overrides this to false regardless of the value passed — PhotoPrism has no Redis integration.

Group 16 — Database Backend

VariableDefaultDescription
database_typeNONEFixed — PhotoPrism has no SQL database (embedded SQLite only). All other database_*/application_database_*/MySQL-plugin variables in this group are inert, forwarded only for foundation compatibility.

Group 19 — Custom Domain, Static IP & Networking

VariableDefaultDescription
enable_custom_domaintrueKubernetes Gateway API + managed certificate.
application_domains[]Custom hostnames; required if enable_custom_domain is left true with no LoadBalancer fallback.
reserve_static_iptrueStable external IP across redeploys.

All other inputs follow standard App_GKE behaviour.


5. Outputs

These values are returned on a successful deployment and are the quickest way to locate and explore the running resources.

OutputDescription
service_nameKubernetes Service name.
namespaceNamespace the workload runs in.
service_cluster_ipIn-cluster ClusterIP.
stage_service_cluster_ipsMap of ClusterIPs for stage-specific services.
service_external_ipExternal LoadBalancer IP (when a static IP is reserved).
service_urlURL to reach PhotoPrism.
photoprism_admin_password_secret_idSecret Manager secret ID holding the generated admin password.
storage_bucketsCreated Cloud Storage buckets.
network_name / network_exists / regionsVPC network, presence, available regions.
container_image / container_registryDeployed image and Artifact Registry repo.
monitoring_enabled / monitoring_notification_channelsMonitoring status and channels.
deployment_id / tenant_id / resource_prefixNaming identifiers.
project_id / project_numberProject identifiers.
initialization_jobsNames of any custom initialization jobs.
statefulset_nameName of the StatefulSet.
cicd_enabled / cicd_configurationCI/CD status and details (repo, trigger, registry).
github_repository_url / github_repository_owner / github_repository_nameCI/CD GitHub details.
artifact_registry_repository / cloudbuild_trigger_name / cloudbuild_trigger_idRegistry and build trigger.
kubernetes_readyWhether the cluster/workload is ready.
vpc_sc_enabled / vpc_sc_perimeter_name / vpc_sc_dry_run_modeVPC-SC status.
audit_logging_enabled / artifact_registry_cmek_enabledAudit logging and CMEK status.

6. Configuration Pitfalls & Sensible Defaults

Risk: Critical (data loss / outage / security) — High (service degraded) — Medium (cost or partial degradation) — Low (minor).

Inherited plan-time validation. This module passes its configuration through the App_GKE foundation engine, which validates values and combinations at plan time — a StatefulSet forced alongside a stateless setting, IAP with no authorized identities, quota_memory_* given as bare integers, an out-of-range container_port/ backup_retention_days. Invalid configuration fails the plan with a clear, named error before any resource is created, so most mistakes below are caught up front rather than at apply or runtime.

SettingSensible valueRiskConsequence if wrong
stateful_pvc_enabledtrueCriticalDisabling it falls back to GCS FUSE, which cannot safely host SQLite or the media index — corruption risk.
max_instance_count1CriticalScaling beyond 1 gives two pods a single writable SQLite DB and PVC — corruption/lock contention.
stateful_pvc_storage_classstandard-rwo (SSD) — consider standard (HDD)Medium–Highstandard-rwo draws the tight regional SSD_TOTAL_GB quota (500GB on Qwiklabs); a campaign of stateful apps can exhaust it around app #8. PhotoPrism does not need SSD IOPS for correctness, only for indexing/thumbnailing throughput — override to HDD (-var stateful_pvc_storage_class=standard) on quota-constrained projects.
enable_redisForced false in main.tfLowNo action needed — the override is intentional and cannot be defeated by setting the variable true.
create_cloud_storagetrueLowThe storage bucket is created but unused while the block PVC is active; harmless, minor idle storage cost.
PHOTOPRISM_ADMIN_PASSWORD (auto-generated)Retrieve before first loginMediumNot knowing it locks you out of the first admin account until reset via the database.
site_urlSet to the deployed URL once knownMediumLeft empty, PhotoPrism falls back to the request host; absolute links/thumbnail URLs can be wrong behind a proxy or custom domain.
stateful_fs_group3000HighA mismatched or unset fsGroup can leave the PVC non-writable by PhotoPrism's UID 1000/GID 2000, blocking startup.
quota_memory_requests / _limitsbinary units (4Gi, 8192Mi)CriticalBare integers are treated as bytes and block all pod scheduling in the namespace.
reserve_static_iptrueMediumWithout it, the external IP can change across redeploys, breaking DNS and site_url.
backup_retention_days7 (raise for prod)MediumToo short for compliance retention.

For the foundation behaviour referenced throughout — IAM and Workload Identity, autoscaling, ingress and certificates, CI/CD, Cloud Armor, IAP, Binary Authorization, VPC-SC, backups, and image mirroring — see App_GKE. PhotoPrism-specific application configuration shared with the Cloud Run variant is described in PhotoPrism_Common.