How to Install PhotoPrism on Ubuntu 24.04 — Self-Hosted AI Photo Library
Your photos are the most personal dataset you own. Birthdays, children's first steps, trips, screenshots of receipts, photos of documents — all of it. When you upload them to Google Photos or iCloud, you're handing that dataset to a company whose business model is advertising or hardware upsells. PhotoPrism flips the equation: you host your own photo library on your own VPS, with TensorFlow-powered AI doing face recognition, object classification, and place detection locally — no external API ever sees your photos.
This guide walks you through a production-grade PhotoPrism install on Ubuntu 24.04 using Docker Compose and MariaDB, then layers on a Nginx reverse proxy with TLS and a proper backup strategy.
Why do this on a CloudCore VPS? PhotoPrism's AI features are RAM hungry. A CloudCore Professional plan at EUR 19.99/month gives you 6 vCPU, 12 GB RAM, and 100 GB NVMe — enough headroom for TensorFlow to index 30,000+ photos without thrashing.
Table of Contents
What is PhotoPrism?
PhotoPrism is an open-source, AI-powered photo management application that you install on your own server. Instead of uploading your memories to a cloud provider, you point PhotoPrism at a folder of photos and it indexes, tags, classifies, and organises them using locally-running machine learning models.
Under the hood PhotoPrism uses TensorFlow for classification (over 20,000 labels across objects, landscapes, animals, and activities), a dedicated face recognition model, a NSFW detector, and an EXIF + XMP metadata pipeline. It understands RAW files from every major camera manufacturer, animated GIFs, live photos, and video thumbnails. The web UI is a polished single-page app with search, albums, maps, a calendar view, and keyboard-driven browsing.
Common use cases include:
- Family photo library — replace Google Photos or iCloud without paying per-GB storage fees.
- RAW workflow backup — photographers offload RAW shoots to the server while keeping JPEG previews for quick browsing.
- Shared household gallery — multiple accounts with per-album permissions so family members can contribute.
- Archive of scanned documents — PhotoPrism's classifier picks up text on documents, receipts, and IDs for fast search.
Why Self-Host Instead of Google Photos or iCloud
The convenience of cloud photo services is real, but so are the trade-offs. Here's the honest comparison:
- Privacy — Your photos never leave your server. PhotoPrism's AI runs on-device (well, on-VPS) via TensorFlow. Google Photos and iCloud scan every image you upload, build biometric profiles of faces, extract text via OCR, and use the aggregate data to train models. When you self-host, none of that happens.
- Unlimited storage at a flat price — Google One charges EUR 9.99/month for 2 TB, EUR 24.99/month for 5 TB, and EUR 49.99/month for 10 TB. iCloud+ is similar. A CloudCore Professional VPS at EUR 19.99/month gives you 100 GB NVMe, and you can attach additional block storage at low per-GB rates to grow without price-tier jumps. A 200 GB library on a CloudCore plan stays cheaper than Google One 2 TB — and you're not renting your own photos back from a megacorp.
- No algorithmic culling or "memories" surveillance — PhotoPrism won't surface a random photo from a painful anniversary on your home screen. You decide what to see.
- Data portability — Your photos live as normal JPEG/RAW/HEIC files in a folder tree on your server. If you stop using PhotoPrism tomorrow, you copy the folder and you're done. No proprietary export tool, no rate-limited Takeout.
- No silent compression — Google Photos historically downscaled "high quality" uploads. PhotoPrism never modifies originals.
- Custom AI pipelines — You control which models run. Face recognition can be disabled per-library. NSFW detection can be tuned. You can plug PhotoPrism into your own classification model if you know what you're doing.
- GDPR-simple — For EU users (or anyone uploading photos of EU citizens) self-hosting eliminates most of the data-transfer analysis you'd otherwise need.
Cost Comparison
| Photo Library Size | Google One | iCloud+ | Self-Hosted PhotoPrism |
|---|---|---|---|
| 100 GB | EUR 1.99/mo | EUR 0.99/mo | EUR 19.99/mo (+ any size up to 100 GB) |
| 500 GB | EUR 2.99/mo | EUR 2.99/mo | EUR 19.99/mo + EUR 5/mo block storage |
| 2 TB | EUR 9.99/mo | EUR 9.99/mo | EUR 19.99/mo + EUR 20/mo block storage |
| 5 TB | EUR 24.99/mo | EUR 29.99/mo | EUR 19.99/mo + EUR 50/mo block storage |
| Privacy scanning | Yes (Google) | Limited (Apple) | None |
| Runs AI locally | No | No | Yes |
Prerequisites
Before starting, make sure you have:
- An Ubuntu 24.04 LTS VPS with root or sudo access.
- Minimum 4 GB RAM, 8 GB+ recommended. TensorFlow and MariaDB together comfortably need 6 GB during indexing.
- At least 40 GB free disk, plus capacity for your photo library. A 30,000-photo iPhone library is roughly 120 GB.
- A domain name (e.g.
photos.example.com) with an A record pointing at the VPS for TLS. - SSH access from your workstation.
Recommended: CloudCore Professional>
For a photo library up to ~100,000 images with all AI features enabled, the CloudCore Professional plan is the sweet spot:>
- 6 vCPU cores
- 12 GB RAM (critical for TensorFlow)
- 100 GB NVMe SSD (expandable with block storage)
- Unmetered bandwidth
- EUR 19.99/month>
Smaller libraries run on smaller plans, but TensorFlow indexing will crawl if you drop below 4 GB RAM.
Connect to your server:
ssh root@your-server-ipStep 1: Prepare the Server
Update the package index and upgrade installed packages:
sudo apt update && sudo apt upgrade -yInstall utilities you'll use later:
sudo apt install -y curl gnupg ca-certificates ufw rsyncOpen the ports you'll need — SSH, HTTP, and HTTPS. We'll keep port 2342 (PhotoPrism's default) closed to the public because all external traffic goes through Nginx.
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
sudo ufw statusIf a kernel update was applied, reboot and reconnect:
sudo rebootStep 2: Install Docker and Docker Compose
PhotoPrism is distributed as an official Docker image, which is by far the easiest and most robust way to run it. Install Docker Engine from Docker's own apt repository so you get the Compose v2 plugin:
sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \ sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpgecho "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Enable Docker on boot and verify the install:
sudo systemctl enable --now docker
sudo docker --version
sudo docker compose versionExpected output:
Docker version 27.0.3, build 7d4bcd8
Docker Compose version v2.29.2Step 3: Create the PhotoPrism Directory Structure
PhotoPrism expects a specific folder layout for originals, imports, storage (thumbnails, sidecar, cache), and the database. Create it under /opt/photoprism:
sudo mkdir -p /opt/photoprism/{originals,import,storage,database}
sudo chown -R 1000:1000 /opt/photoprism
cd /opt/photoprismMeaning of each folder:
/opt/photoprism/originals— Your photo library. This is the folder PhotoPrism indexes and serves. Treat this as the source of truth./opt/photoprism/import— A staging folder. Drop new photos here and use the UI's "Import" action to move them intooriginalswith proper date-based subfolders./opt/photoprism/storage— PhotoPrism's working directory: thumbnail cache, sidecar YAML, index DB backups. Safe to lose (will be rebuilt) but backing it up saves re-indexing time./opt/photoprism/database— MariaDB's data directory. Must be backed up.
Step 4: Write the .env File
Using a .env file keeps secrets out of docker-compose.yml so you can safely check the compose file into git later. Create it:
sudo tee /opt/photoprism/.env > /dev/null <<'EOF'
====================
PhotoPrism .env
====================
Admin credentials — change these before first launch
PHOTOPRISM_ADMIN_USER=admin
PHOTOPRISM_ADMIN_PASSWORD=ReplaceMeWithALongRandomStringPublic URL (update after Nginx/TLS is in place)
PHOTOPRISM_SITE_URL=https://photos.example.com/
PHOTOPRISM_SITE_TITLE=My Photo LibraryDatabase
MARIADB_ROOT_PASSWORD=AnotherLongRandomString
PHOTOPRISM_DATABASE_NAME=photoprism
PHOTOPRISM_DATABASE_USER=photoprism
PHOTOPRISM_DATABASE_PASSWORD=YetAnotherRandomStringPerformance tuning
PHOTOPRISM_WORKERS=4
EOF
sudo chmod 600 /opt/photoprism/.envGenerate strong random passwords before committing this file — openssl rand -base64 32 produces a suitable string.
Security note: The admin password is only read on first launch. Changing it in the.envafterwards does nothing; change it from the Settings UI or viadocker compose exec photoprism photoprism users mod admin --password.
Step 5: Write docker-compose.yml
Create the Compose file that defines two services — the PhotoPrism app and a MariaDB database:
sudo tee /opt/photoprism/docker-compose.yml > /dev/null <<'EOF' services: photoprism: image: photoprism/photoprism:latest container_name: photoprism restart: unless-stopped stop_grace_period: 10s depends_on: - mariadb security_opt: - seccomp:unconfined - apparmor:unconfined ports: - "127.0.0.1:2342:2342" environment: PHOTOPRISM_ADMIN_USER: "${PHOTOPRISM_ADMIN_USER}" PHOTOPRISM_ADMIN_PASSWORD: "${PHOTOPRISM_ADMIN_PASSWORD}" PHOTOPRISM_AUTH_MODE: "password" PHOTOPRISM_SITE_URL: "${PHOTOPRISM_SITE_URL}" PHOTOPRISM_SITE_TITLE: "${PHOTOPRISM_SITE_TITLE}" PHOTOPRISM_ORIGINALS_LIMIT: 5000 PHOTOPRISM_HTTP_COMPRESSION: "gzip" PHOTOPRISM_LOG_LEVEL: "info" PHOTOPRISM_READONLY: "false" PHOTOPRISM_EXPERIMENTAL: "false" PHOTOPRISM_DISABLE_CHOWN: "false" PHOTOPRISM_DISABLE_WEBDAV: "false" PHOTOPRISM_DISABLE_SETTINGS: "false" PHOTOPRISM_DISABLE_TENSORFLOW: "false" PHOTOPRISM_DISABLE_FACES: "false" PHOTOPRISM_DISABLE_CLASSIFICATION: "false" PHOTOPRISM_DISABLE_VECTORS: "false" PHOTOPRISM_DISABLE_RAW: "false" PHOTOPRISM_RAW_PRESETS: "false" PHOTOPRISM_JPEG_QUALITY: 85 PHOTOPRISM_DETECT_NSFW: "true" PHOTOPRISM_UPLOAD_NSFW: "true" PHOTOPRISM_DATABASE_DRIVER: "mysql" PHOTOPRISM_DATABASE_SERVER: "mariadb:3306" PHOTOPRISM_DATABASE_NAME: "${PHOTOPRISM_DATABASE_NAME}" PHOTOPRISM_DATABASE_USER: "${PHOTOPRISM_DATABASE_USER}" PHOTOPRISM_DATABASE_PASSWORD: "${PHOTOPRISM_DATABASE_PASSWORD}" PHOTOPRISM_WORKERS: "${PHOTOPRISM_WORKERS}" PHOTOPRISM_PLACES_ENABLED: "true" TZ: "Europe/Berlin" user: "1000:1000" working_dir: "/photoprism" volumes: - "./originals:/photoprism/originals" - "./import:/photoprism/import" - "./storage:/photoprism/storage"
mariadb: image: mariadb:11 container_name: photoprism-mariadb restart: unless-stopped stop_grace_period: 5s security_opt: - seccomp:unconfined - apparmor:unconfined command: - mariadbd - --innodb-buffer-pool-size=512M - --transaction-isolation=READ-COMMITTED - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci - --max-connections=512 - --innodb-rollback-on-timeout=OFF - --innodb-lock-wait-timeout=120 volumes: - "./database:/var/lib/mysql" environment: MARIADB_AUTO_UPGRADE: "1" MARIADB_INITDB_SKIP_TZINFO: "1" MARIADB_DATABASE: "${PHOTOPRISM_DATABASE_NAME}" MARIADB_USER: "${PHOTOPRISM_DATABASE_USER}" MARIADB_PASSWORD: "${PHOTOPRISM_DATABASE_PASSWORD}" MARIADB_ROOT_PASSWORD: "${MARIADB_ROOT_PASSWORD}" EOF
Key things to note in this file:
ports: 127.0.0.1:2342:2342— PhotoPrism only listens on localhost. All outside traffic must come through Nginx. This is deliberate; never bind it to0.0.0.0unless you fully understand what you're doing./photoprism/originals— The volume where your photos live. This is the critical folder to back up.PHOTOPRISM_DISABLE_TENSORFLOW: false— Leaves AI on. Set totrueto save ~1.5 GB RAM if you don't want classification/faces/NSFW.PHOTOPRISM_WORKERS: 4— Number of indexing workers. Match this to your vCPU count.
Step 6: Launch the Stack
From /opt/photoprism, pull images and start the containers:
sudo docker compose pull
sudo docker compose up -dWatch the logs for 30 seconds to make sure initial DB migrations succeed:
sudo docker compose logs -f photoprismYou should see lines like:
time="2026-04-16T10:10:04Z" level=info msg="config: successfully initialized..."
time="2026-04-16T10:10:09Z" level=info msg="migrate: successfully applied 42 migrations..."
time="2026-04-16T10:10:11Z" level=info msg="started web server at 0.0.0.0:2342"Press Ctrl+C to stop following logs. Confirm both containers are up:
sudo docker compose psExpected:
NAME IMAGE STATUS PORTS
photoprism photoprism/photoprism:latest Up (healthy) 127.0.0.1:2342->2342/tcp
photoprism-mariadb mariadb:11 Up 3306/tcpSmoke-test the HTTP endpoint locally:
curl -I http://127.0.0.1:2342You should get a 200 OK response.
Step 7: First Login and Library Import
Before exposing the app to the world, test-drive it over an SSH tunnel. From your workstation:
ssh -L 2342:127.0.0.1:2342 root@your-server-ipNow open http://localhost:2342 in your browser. You'll see the PhotoPrism login. Sign in with the admin credentials from your .env.
Importing your first photos
There are two ways to add photos:
/opt/photoprism/originals directly./opt/photoprism/originals via SCP/rsync. Then in the UI go to Library → Index and click Start.For a large migration from Google Takeout, the second approach is dramatically faster. From your workstation:
rsync -avzP ~/GoogleTakeout/ root@your-server-ip:/opt/photoprism/originals/Then in the UI: Library → Index → Complete Rescan → Start. A 10,000-photo initial index on 4 vCPUs typically runs 2-6 hours. Subsequent scans only touch new files.
Library layout
PhotoPrism respects your existing folder structure under originals/. If you don't have one, it will build a YYYY/MM/ hierarchy as you import. Either approach works — pick whichever you'll recognise in five years.
Step 8: Enable AI Features (Faces, NSFW, Places)
AI features are enabled in the Compose file above, but they're also toggleable per-library at runtime.
Face recognition (TensorFlow)
Go to Settings → Library and confirm Faces is enabled. Then:
Face recognition uses the InsightFace ArcFace model, bundled with the container. No external API calls. On 4 vCPUs, face embedding adds roughly 15-25% to indexing time.
NSFW detection
NSFW detection is enabled via PHOTOPRISM_DETECT_NSFW: "true" in the Compose file. PhotoPrism classifies every photo using a dedicated TensorFlow model and flags borderline content. You can view flagged photos under Labels → Explicit and moderate them.
If you want to reject NSFW uploads rather than just flag them, change PHOTOPRISM_UPLOAD_NSFW: "false" and restart the stack.
Classification labels
Over 20,000 labels are generated automatically during indexing — "mountain", "beach", "golden retriever", "birthday cake". Search by label from the top search bar or via URL: /all?q=label:mountain.
Places (reverse geocoding)
The Places feature turns GPS EXIF data into human-readable locations (country, region, city) and plots photos on a map. It uses PhotoPrism's public Places API. If you'd rather keep your GPS metadata entirely on-server, set PHOTOPRISM_PLACES_ENABLED: "false" and restart. The map view still works using raw coordinates, it just won't reverse-geocode.
Step 9: Nginx Reverse Proxy with TLS
Time to expose PhotoPrism on your domain. We'll install Nginx, obtain a Let's Encrypt certificate with Certbot, and terminate TLS at Nginx.
Install Nginx and Certbot
sudo apt install -y nginx certbot python3-certbot-nginxCreate the site config
sudo tee /etc/nginx/sites-available/photoprism > /dev/null <<'EOF' server { listen 80; server_name photos.example.com; return 301 https://$host$request_uri; }server { listen 443 ssl http2; server_name photos.example.com;
# Certbot will populate these paths ssl_certificate /etc/letsencrypt/live/photos.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/photos.example.com/privkey.pem;
# Hardened defaults ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; add_header X-Content-Type-Options nosniff; add_header X-Frame-Options SAMEORIGIN;
# PhotoPrism uploads can be large (RAW files, videos) client_max_body_size 2G; client_body_timeout 600s; send_timeout 600s;
# Buffering off for WebSocket-style upload progress proxy_buffering off; proxy_request_buffering off;
location / { proxy_pass http://127.0.0.1:2342; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 600s; } } EOF
sudo ln -s /etc/nginx/sites-available/photoprism /etc/nginx/sites-enabled/ sudo nginx -t
Obtain the certificate
Temporarily make the HTTP server serve real content so Certbot's HTTP-01 challenge succeeds. Easiest method — run Certbot in nginx mode and let it edit the config for you:
sudo certbot --nginx -d photos.example.comFollow the prompts. Certbot will obtain the certificate, update the Nginx config with the real paths, and configure auto-renewal via systemd timer.
Reload Nginx:
sudo systemctl reload nginxUpdate PhotoPrism's site URL
Edit /opt/photoprism/.env and set:
PHOTOPRISM_SITE_URL=https://photos.example.com/Restart the container:
cd /opt/photoprism
sudo docker compose up -dBrowse to https://photos.example.com. You should see the login page over TLS. Sign in and verify everything works — thumbnails, playback, upload.
Step 10: Backups — Originals and Database
A photo library without backups is a library waiting to be lost. PhotoPrism needs two things backed up:
/opt/photoprism/originals) — your actual photos.Optionally, the storage folder (sidecar YAMLs and thumbnails) saves re-indexing time on restore.
Create a backup script
sudo tee /opt/photoprism/backup.sh > /dev/null <<'EOF' #!/usr/bin/env bash set -euo pipefailBACKUP_ROOT="/var/backups/photoprism" STAMP=$(date +%Y-%m-%d) DEST="${BACKUP_ROOT}/${STAMP}"
mkdir -p "${DEST}"
1. Dump MariaDB
docker compose -f /opt/photoprism/docker-compose.yml exec -T mariadb \ mariadb-dump \ --user=root \ --password="${MARIADB_ROOT_PASSWORD}" \ --single-transaction \ --routines \ photoprism | gzip > "${DEST}/photoprism.sql.gz"2. Sync originals (incremental, hardlinks unchanged files)
rsync -aH --delete \ /opt/photoprism/originals/ \ "${BACKUP_ROOT}/originals-current/"3. Sync sidecar metadata (optional but small & useful)
rsync -aH --delete \ /opt/photoprism/storage/sidecar/ \ "${BACKUP_ROOT}/sidecar-current/" 2>/dev/null || true4. Keep 14 nightly DB dumps, prune older
find "${BACKUP_ROOT}" -maxdepth 1 -type d -name '20*' -mtime +14 -exec rm -rf {} +echo "Backup complete: ${DEST}" EOF
sudo chmod +x /opt/photoprism/backup.sh
The script sources the root password from the environment — export it when running, or source your .env:
sudo bash -c 'set -a; source /opt/photoprism/.env; /opt/photoprism/backup.sh'Schedule it nightly
Add a cron entry:
sudo crontab -eAppend:
0 3 * set -a; . /opt/photoprism/.env; /opt/photoprism/backup.sh >> /var/log/photoprism-backup.log 2>&1Push backups offsite
A local backup saves you from accidental deletion but not from disk failure. Push /var/backups/photoprism to offsite storage — S3-compatible object storage, a second VPS, or a home NAS. A simple offsite step using rclone:
sudo apt install -y rclone
sudo rclone config # configure your remote, e.g. Backblaze B2 or WasabiAdd to the backup script after the local sync:
rclone sync "${BACKUP_ROOT}" remote:photoprism-backups --fast-listRestore procedure
On a fresh server:
docker-compose.yml + .env.originals-current/ tree back into /opt/photoprism/originals/.docker compose up -d mariadb.gunzip -c photoprism.sql.gz | docker compose exec -T mariadb \
mariadb --user=root --password="$MARIADB_ROOT_PASSWORD" photoprismdocker compose up -d photoprism.Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
Container restarts with database connection failed | MariaDB not ready yet, or wrong password | docker compose logs mariadb. If passwords drifted, stop stack, delete /opt/photoprism/database, re-start (this wipes the DB — only do on fresh installs). |
| Indexing stalls at 0% | TensorFlow OOM killed | Check dmesg</td><td>grep -i kill<code>. Add swap: </code>sudo fallocate -l 4G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile. Or upgrade RAM. |
| Uploads fail at ~100 MB | Nginx client_max_body_size too small | Already set to 2G in the config above. If you changed it, bump higher and sudo systemctl reload nginx. |
| 502 Bad Gateway from Nginx | PhotoPrism container down | sudo docker compose ps. If unhealthy, sudo docker compose logs photoprism. |
| Faces are empty after re-index | Face detection disabled or library not re-indexed | Settings → Library → ensure Faces on. Re-run index with Complete Rescan. |
| Places shows "unknown" for every photo | No GPS EXIF on photos, or Places disabled | Check EXIF: exiftool photo.jpg</td><td>grep GPS<code>. Enable </code>PHOTOPRISM_PLACES_ENABLED=true. |
| WebDAV can't connect from iPhone | Nginx missing WebDAV methods | The config above proxies all methods. In iOS WebDAV client, use URL https://photos.example.com/originals/ with admin credentials. |
| Disk full after a year | Thumbnails and sidecars grow | Check du -sh /opt/photoprism/storage. Clear cache: docker compose exec photoprism photoprism thumbs --recreate. Or attach block storage and move storage/. |
FAQ
Is PhotoPrism free?
PhotoPrism Community Edition is free and open source under the AGPLv3 license. Face recognition, AI classification, NSFW detection, Places, mobile sync via WebDAV — all of it is free. PhotoPrism also offers a paid Plus membership that adds enterprise-grade permissions and supports development, but it is not required for personal or family use.
How much RAM do I need?
The official docs recommend 4 GB as the bare minimum. In practice, TensorFlow spikes memory usage dramatically during indexing — I've seen 5-6 GB consumed on a fresh import of 10,000 photos. 8 GB is a comfortable minimum, and 12 GB (as on the CloudCore Professional plan) is the sweet spot for libraries up to around 100,000 photos. If you're running other services on the same VPS (Nextcloud, media server, etc.), stack your RAM requirements.
PhotoPrism vs Immich — which should I pick?
Both are excellent and both run on Ubuntu with Docker. PhotoPrism has a longer track record, handles RAW files and large libraries very well, and its AI classification labels are more extensive. Immich has a more polished mobile app and newer UI. If your priority is iPhone/Android auto-backup with a slick app, try Immich. If classification, RAW workflows, and library search matter more, PhotoPrism is the better pick. You can run both on the same VPS and decide.
Does PhotoPrism work without the internet?
Yes, almost entirely. TensorFlow, face recognition, and NSFW detection all run locally inside the container — no external API calls. The only feature that reaches the internet is Places (reverse geocoding via the PhotoPrism Places API), and it's optional. Set PHOTOPRISM_PLACES_ENABLED=false for fully airgapped operation.
Can I upload from iPhone or Android?
Yes. PhotoPrism exposes a WebDAV endpoint at /originals/. Any WebDAV-capable mobile app works — PhotoSync on iOS is the most popular pick, FolderSync on Android. The official PhotoPrism mobile apps are in active development but WebDAV is the reliable path today. Configure your WebDAV URL as https://photos.example.com/originals/ with your admin credentials and set auto-upload.
How do I back up my library?
Two things need backup: the /opt/photoprism/originals folder (your photos) and the MariaDB database (indexes, faces, albums). The backup script in Step 10 handles both — mysqldump for the DB, rsync for the originals — on a nightly cron. For durability, sync the backup root to an offsite location with rclone to S3/Backblaze. The storage/sidecar folder is worth including too; it contains per-photo YAML metadata that survives re-indexing.
Why does indexing take so long?
The first index runs TensorFlow against every file for classification, extracts face embeddings, checks for NSFW, generates thumbnails, and writes the sidecar YAML. On 4 vCPUs, expect 2-6 hours for 10,000 photos. On 8 vCPUs it roughly halves. Subsequent scans are incremental — PhotoPrism only processes new or modified files and completes in seconds for a handful of added photos. If performance matters, set PHOTOPRISM_WORKERS to match your vCPU count and avoid running other CPU-heavy workloads on the same VPS during the initial index.
Next Steps
With PhotoPrism running, you have a private, AI-powered photo library that you fully control. Complement it:
- Install Immich alongside for iPhone auto-backup — Run both on the same VPS. See how to install Immich on Ubuntu 24.04. Use Immich's mobile app as a capture pipeline, rsync into PhotoPrism for deep indexing.
- Add Nextcloud for full cloud storage — PhotoPrism handles photos; Nextcloud handles documents, calendar, contacts, and file sync across devices. Together they cover the full Google/iCloud replacement surface.
- Install Jellyfin for the video side — Home videos belong in PhotoPrism, but movies and TV ripped from your collection deserve a proper media server. Jellyfin on Ubuntu is the open-source Plex alternative that runs alongside PhotoPrism on the same VPS.
- Set up Uptime Kuma — Monitor
https://photos.example.comand alert via Telegram, email, or Discord if PhotoPrism goes down. - Read the official PhotoPrism docs — The docs.photoprism.app site has deep references on config options, advanced search syntax, RAW conversion, and developer APIs.
Ready to host your photos?>
The CloudCore Professional VPS is the plan we built this guide around — 6 vCPU, 12 GB RAM, 100 GB NVMe for EUR 19.99/month. Enough RAM for TensorFlow, enough SSD speed for thumbnails, and enough bandwidth to stream RAW previews without hitches.>
Deploy in under 60 seconds, follow this guide, and in under an hour your photos are off Google Photos and onto infrastructure you control.