Why self-host PhotoPrism on a VPS
Where Immich aims for the full cloud experience with real-time mobile backup, PhotoPrism focuses on indexing and organizing an existing photo library, often large, sitting on a file system. It analyzes your folders, sorts photos by labels (mountain, beach, car), detects faces, reads EXIF and geocodes pictures. All of this runs locally via embedded TensorFlow. Hosting it on a VPS has a twofold benefit: centralizing photo archives scattered on a disk accessible from anywhere, and getting automatic tagging without ever transmitting the images to a third-party service. PhotoPrism works with MariaDB for good performance and directly reads an originals folder that you fill via WebDAV, rsync or a mount.
The strengths of a self-hosted PhotoPrism
- Automatic tagging by local AI: search by keywords, locations and colors without a cloud.
- Indexing of existing archives placed on disk, via WebDAV, rsync or network mount.
- Detection of duplicates and similar photos to clean up a cluttered collection.
- Interactive maps based on the GPS coordinates extracted from EXIF metadata.
- Read-only mode available: PhotoPrism indexes without ever modifying your original files.
- Installable PWA interface, accessible from any browser on your network.
Exact requirements: RAM, CPU, storage and networking
PhotoPrism is memory-hungry as soon as TensorFlow is involved. Here are the thresholds to respect to avoid unpleasant surprises. Without a GPU and with the ML engine active, plan for 2 GB of RAM minimum: below that, the indexing process triggers an OOM (out of memory) and the container restarts in a loop. With ML fully active (face detection, scene classification), 4 GB of RAM are recommended — the comfortable level for a library of 10,000 to 50,000 photos. A minimum 1 vCPU is enough for browsing, but the initial indexing of a large archive benefits from 2 to 4 cores: PhotoPrism distributes work via PHOTOPRISM_WORKERS. The default port is 2342; remember to open it temporarily for a first test before placing the reverse proxy. On the storage side, provision your originals (the actual size of your library) plus 30 to 40% for multi-resolution thumbnails, cache and the MariaDB index. A library of 100 GB of originals typically generates between 20 and 35 GB of derived data. Add a 2 GB minimum swap partition if your VPS has less than 4 GB of RAM: it absorbs indexing spikes without crashes, at the cost of slight slowness.
Deploy PhotoPrism step by step
Prepare the VPS and volumes
Install Docker, then create two persistent folders:
/opt/photoprism/originalsfor your photos and/opt/photoprism/storagefor the cache and the index database. Separating these volumes simplifies backups.Declare PhotoPrism and MariaDB
In
docker-compose.yml, add amariadbservice with a dedicated volume and aphotoprismservice (imagephotoprism/photoprism:latest). Link them viaPHOTOPRISM_DATABASE_DRIVER=mysqland pointPHOTOPRISM_ORIGINALS_PATHto your photo folder.Set the administrator and the indexing mode
Set
PHOTOPRISM_ADMIN_PASSWORDand enablePHOTOPRISM_READONLY=trueif you want it to index without touching the originals. Then launch the stack withdocker compose up -d.Import and run the first indexing
Copy your photos into the
originalsfolder viarsyncor WebDAV, then trigger the analysis from the interface or on the command line withdocker compose exec photoprism photoprism index. This is where the AI generates labels and thumbnails.Configure nginx as a reverse proxy with SSL Certbot
Install nginx and certbot on the host. Create a server block in
/etc/nginx/sites-available/photoprism.confthat listens on port 80, redirects to 443, and proxies tohttp://127.0.0.1:2342. Add the directivesproxy_set_header Host $host,proxy_set_header X-Real-IP $remote_addrandproxy_set_header X-Forwarded-Proto $schemeso PhotoPrism correctly detects the HTTPS scheme. Obtain the certificate withcertbot --nginx -d gallery.yourdomain.com. Automatic renewal is handled by the certbot systemd timer — check withsystemctl status certbot.timer.
Mobile access: iOS and Android app
PhotoPrism does not offer a native automatic backup app like Immich, but its PWA (Progressive Web App) interface is mobile-optimized: add it to your home screen from Safari (iOS) or Chrome (Android) for a near-native experience. For browsing and searching, this is sufficient. If you want access outside the local network, enable HTTPS (see the reverse proxy step): the PWA only installs in a secure context. For backup from the phone, the usual complementary solution is Syncthing or rclone: configure them to send the DCIM folder to /opt/photoprism/originals on the VPS, then schedule automatic re-indexing via a cron running docker compose exec -T photoprism photoprism index --cleanup.
ML optimization on a VPS without GPU: key variables
On a standard VPS (no dedicated GPU), TensorFlow runs in CPU mode. Three environment variables let you adjust the quality/resource balance. PHOTOPRISM_WORKERS controls the number of parallel indexing threads — set it to the number of available vCPUs (2 or 4) to speed up initial indexing without saturating the CPU. On a single-vCPU VPS, force it to 1 to avoid contention. PHOTOPRISM_DISABLE_TENSORFLOW (value true) completely disables the ML engine: no more automatic classification or face detection, but indexing is 3 to significantly faster for indexing and RAM consumption drops below 1 GB. Useful for an initial migration of a very large archive — re-enable ML afterwards to re-run the analysis. PHOTOPRISM_DETECT_NSFW (value true) enables the inappropriate content filter, at the cost of an extra model pass on each image: only enable it if you genuinely need it, as it significantly slows indexing. For VPSes between 2 and 4 GB of RAM, the recommended configuration is PHOTOPRISM_WORKERS=2, PHOTOPRISM_DETECT_NSFW=false and TensorFlow active — you get ML without OOM risk.
Troubleshooting: the most common errors
OOM (Out of Memory) at startup or during indexing. This is the first error on VPSes with 1 GB of RAM. The Docker log signature: Killed or signal: killed on the photoprism process. Immediate fix: add swap (fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile) and add the line to /etc/fstab for persistence. Lasting fix: move to a VPS with at least 2 GB. Endless indexing that never finishes. On a library of 50,000 photos on a 2-vCPU VPS, the first complete indexing (with ML) can take 6 to 24 hours. This is normal. Follow progress in the logs with docker compose logs -f photoprism. If the process seems stuck for more than an hour, check CPU usage: docker stats photoprism. If it is at 0%, restart the service. Permission problems on volumes. PhotoPrism runs as UID 1000 by default. If /opt/photoprism/originals is owned by root, access silently fails. Fix with chown -R 1000:1000 /opt/photoprism. Also verify that the storage volume is writable: this is where PhotoPrism writes thumbnails and the local index. Interface loads but photos do not appear. Check that indexing was actually triggered (photoprism index) and not just the service started. The initial import is not automatic.
Backup strategy: originals, storage and database
An effective PhotoPrism backup distinguishes three components with different needs. The originals folder contains your master files (JPEG, RAW, videos): this is the irreplaceable part. Back it up first with an off-site copy — rclone sync /opt/photoprism/originals remote:backup/originals to object storage (Backblaze B2, S3) works well and supports incremental resumption. Recommended frequency: daily. The storage folder contains generated thumbnails, cache and sidecar data. It can be fully rebuilt by re-indexing (photoprism index --force): back it up only if you want to avoid re-running a long indexing after an incident. A weekly snapshot is sufficient. The MariaDB database stores the metadata index, albums, manual labels and identified faces — all the curation work you have done manually. Back it up separately with a regular dump: docker compose exec mariadb mysqldump -u photoprism -p photoprism > /opt/backup/photoprism-db-$(date +%Y%m%d).sql. Schedule this dump in cron and encrypt it before off-site transfer. If you use SQLite instead of MariaDB, simply copy the .db file after stopping PhotoPrism to guarantee consistency.
PhotoPrism or Immich: which to choose
Scroll the table
| Criterion | PhotoPrism | Immich |
|---|---|---|
| Main objective | Index and organize an existing photo library | Full photo cloud with mobile backup |
| Mobile app | PWA, no native automatic backup | iOS/Android apps with automatic backup |
| Database | MariaDB or SQLite | PostgreSQL with vector extension |
| AI search | TensorFlow labels by content and color | Semantic search and advanced facial recognition |
| Recommended RAM | 4 GB (2 GB minimum without GPU) | 6 to 8 GB |
| File model | Reads an originals folder, read-only mode available | Manages its own upload tree |
| Multi-user | Limited, geared toward a single collection | Isolated libraries per user |
| Ideal use case | Archive and search large photo folders | Replace Google Photos day to day |
Mount your originals folder read-only (:ro in the Docker volume) and enable PHOTOPRISM_READONLY=true. PhotoPrism will index and generate labels and thumbnails in the separate storage volume, without ever risking modifying or deleting an original file. This way you can safely point to a precious archive, and even synchronize that folder via rsync from another server.