Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Photoprism Ubuntu
GUIDEInstall Guides

How to Install PhotoPrism on Ubuntu 24.04 — Self-Hosted AI Photo Library

25 min read

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?
  • Why Self-Host Instead of Google Photos or iCloud
  • Prerequisites
  • Step 1: Prepare the Server
  • Step 2: Install Docker and Docker Compose
  • Step 3: Create the PhotoPrism Directory Structure
  • Step 4: Write the .env File
  • Step 5: Write docker-compose.yml
  • Step 6: Launch the Stack
  • Step 7: First Login and Library Import
  • Step 8: Enable AI Features (Faces, NSFW, Places)
  • Step 9: Nginx Reverse Proxy with TLS
  • Step 10: Backups — Originals and Database
  • Troubleshooting
  • FAQ
  • Next Steps
  • 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.
    PhotoPrism is developed actively on GitHub, has a Community Edition that is fully featured for personal and small-team use, and a paid Plus membership that adds enterprise-grade permissions.

    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 SizeGoogle OneiCloud+Self-Hosted PhotoPrism
    100 GBEUR 1.99/moEUR 0.99/moEUR 19.99/mo (+ any size up to 100 GB)
    500 GBEUR 2.99/moEUR 2.99/moEUR 19.99/mo + EUR 5/mo block storage
    2 TBEUR 9.99/moEUR 9.99/moEUR 19.99/mo + EUR 20/mo block storage
    5 TBEUR 24.99/moEUR 29.99/moEUR 19.99/mo + EUR 50/mo block storage
    Privacy scanningYes (Google)Limited (Apple)None
    Runs AI locallyNoNoYes
    Below ~500 GB, Google and iCloud are cheaper on pure storage. But if you value privacy, want to stop paying rent on your own photos, or already have a VPS for other workloads, self-hosting wins quickly.

    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:

    bash
    ssh root@your-server-ip

    Step 1: Prepare the Server

    Update the package index and upgrade installed packages:

    bash
    sudo apt update && sudo apt upgrade -y

    Install utilities you'll use later:

    bash
    sudo apt install -y curl gnupg ca-certificates ufw rsync

    Open 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.

    bash
    sudo ufw allow OpenSSH
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    sudo ufw --force enable
    sudo ufw status

    If a kernel update was applied, reboot and reconnect:

    bash
    sudo reboot

    Step 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:

    bash
    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.gpg

    echo "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:

    bash
    sudo systemctl enable --now docker
    sudo docker --version
    sudo docker compose version

    Expected output:

    text
    Docker version 27.0.3, build 7d4bcd8
    Docker Compose version v2.29.2

    Step 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:

    bash
    sudo mkdir -p /opt/photoprism/{originals,import,storage,database}
    sudo chown -R 1000:1000 /opt/photoprism
    cd /opt/photoprism

    Meaning 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 into originals with 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:

    bash
    sudo tee /opt/photoprism/.env > /dev/null <<'EOF'
    

    ====================

    PhotoPrism .env

    ====================

    Admin credentials — change these before first launch

    PHOTOPRISM_ADMIN_USER=admin PHOTOPRISM_ADMIN_PASSWORD=ReplaceMeWithALongRandomString

    Public URL (update after Nginx/TLS is in place)

    PHOTOPRISM_SITE_URL=https://photos.example.com/ PHOTOPRISM_SITE_TITLE=My Photo Library

    Database

    MARIADB_ROOT_PASSWORD=AnotherLongRandomString PHOTOPRISM_DATABASE_NAME=photoprism PHOTOPRISM_DATABASE_USER=photoprism PHOTOPRISM_DATABASE_PASSWORD=YetAnotherRandomString

    Performance tuning

    PHOTOPRISM_WORKERS=4 EOF sudo chmod 600 /opt/photoprism/.env

    Generate 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 .env afterwards does nothing; change it from the Settings UI or via docker 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:

    bash
    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 to 0.0.0.0 unless 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 to true to 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:

    bash
    sudo docker compose pull
    sudo docker compose up -d

    Watch the logs for 30 seconds to make sure initial DB migrations succeed:

    bash
    sudo docker compose logs -f photoprism

    You should see lines like:

    text
    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:

    bash
    sudo docker compose ps

    Expected:

    text
    NAME                 IMAGE                        STATUS         PORTS
    photoprism           photoprism/photoprism:latest Up (healthy)   127.0.0.1:2342->2342/tcp
    photoprism-mariadb   mariadb:11                   Up             3306/tcp

    Smoke-test the HTTP endpoint locally:

    bash
    curl -I http://127.0.0.1:2342

    You 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:

    bash
    ssh -L 2342:127.0.0.1:2342 root@your-server-ip

    Now 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:

  • Upload via UI — Use the upload button. Files land in /opt/photoprism/originals directly.
  • Drop files on the server, then index — Copy your library into /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:

    bash
    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:

  • Navigate to Library → Index, check Complete Rescan, click Start. PhotoPrism will extract face embeddings for every photo.
  • After indexing finishes, go to People. You'll see face clusters.
  • Click a cluster and name the person. PhotoPrism merges matching clusters automatically and keeps learning.
  • 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

    bash
    sudo apt install -y nginx certbot python3-certbot-nginx

    Create the site config

    bash
    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:

    bash
    sudo certbot --nginx -d photos.example.com

    Follow the prompts. Certbot will obtain the certificate, update the Nginx config with the real paths, and configure auto-renewal via systemd timer.

    Reload Nginx:

    bash
    sudo systemctl reload nginx

    Update PhotoPrism's site URL

    Edit /opt/photoprism/.env and set:

    text
    PHOTOPRISM_SITE_URL=https://photos.example.com/

    Restart the container:

    bash
    cd /opt/photoprism
    sudo docker compose up -d

    Browse 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:

  • The originals folder (/opt/photoprism/originals) — your actual photos.
  • The MariaDB database — indexes, face clusters, albums, labels.
  • Optionally, the storage folder (sidecar YAMLs and thumbnails) saves re-indexing time on restore.

    Create a backup script

    bash
    sudo tee /opt/photoprism/backup.sh > /dev/null <<'EOF'
    #!/usr/bin/env bash
    set -euo pipefail

    BACKUP_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 || true

    4. 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:

    bash
    sudo bash -c 'set -a; source /opt/photoprism/.env; /opt/photoprism/backup.sh'

    Schedule it nightly

    Add a cron entry:

    bash
    sudo crontab -e

    Append:

    text
    0 3   * set -a; . /opt/photoprism/.env; /opt/photoprism/backup.sh >> /var/log/photoprism-backup.log 2>&1

    Push 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:

    bash
    sudo apt install -y rclone
    sudo rclone config   # configure your remote, e.g. Backblaze B2 or Wasabi

    Add to the backup script after the local sync:

    bash
    rclone sync "${BACKUP_ROOT}" remote:photoprism-backups --fast-list

    Restore procedure

    On a fresh server:

  • Reinstall Docker, Nginx, and redeploy the same docker-compose.yml + .env.
  • Copy the originals-current/ tree back into /opt/photoprism/originals/.
  • Start only MariaDB: docker compose up -d mariadb.
  • Import the SQL dump:
  • bash
    gunzip -c photoprism.sql.gz | docker compose exec -T mariadb \
         mariadb --user=root --password="$MARIADB_ROOT_PASSWORD" photoprism
  • Start PhotoPrism: docker compose up -d photoprism.
  • Log in; everything — albums, faces, labels — is back.
  • Troubleshooting

    ProblemCauseSolution
    Container restarts with database connection failedMariaDB not ready yet, or wrong passworddocker 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 killedCheck 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 MBNginx client_max_body_size too smallAlready set to 2G in the config above. If you changed it, bump higher and sudo systemctl reload nginx.
    502 Bad Gateway from NginxPhotoPrism container downsudo docker compose ps. If unhealthy, sudo docker compose logs photoprism.
    Faces are empty after re-indexFace detection disabled or library not re-indexedSettings → Library → ensure Faces on. Re-run index with Complete Rescan.
    Places shows "unknown" for every photoNo GPS EXIF on photos, or Places disabledCheck EXIF: exiftool photo.jpg</td><td>grep GPS<code>. Enable </code>PHOTOPRISM_PLACES_ENABLED=true.
    WebDAV can't connect from iPhoneNginx missing WebDAV methodsThe config above proxies all methods. In iOS WebDAV client, use URL https://photos.example.com/originals/ with admin credentials.
    Disk full after a yearThumbnails and sidecars growCheck 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.com and 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.

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket