$linuxjunkies
>

Self-Host PhotoPrism for Photo Library

Deploy PhotoPrism on your own server with Docker Compose. Covers storage layout, database setup, face recognition tuning, and reverse geocoding for places.

IntermediateUbuntuDebianFedoraArch12 min readUpdated June 7, 2026

Before you start

  • A Linux server or VM with at least 4 GB RAM and 20 GB free disk space beyond your photo collection
  • Root or sudo access to install Docker
  • Your photo library accessible as a local directory or mounted network share
  • Basic familiarity with Docker concepts (images, volumes, environment variables)

PhotoPrism is a privacy-respecting, AI-powered photo manager you can run entirely on your own hardware. It handles face recognition, reverse-geocoded locations, automatic categorisation, and RAW conversion without sending a single pixel to a third-party cloud. This guide walks through a production-ready Docker Compose deployment, covers the storage layout you must get right on day one, and explains how to tune indexing for faces and places.

Prerequisites and Hardware Expectations

PhotoPrism is not lightweight. Indexing a large library with faces and places enabled saturates a CPU for hours. Plan accordingly:

  • A host with at least 4 GB RAM (8 GB recommended for libraries over 50k photos).
  • Docker Engine 24+ and the Docker Compose plugin (docker compose, not the legacy docker-compose binary).
  • A dedicated data volume with enough space for originals plus roughly 10–30% extra for sidecar files and preview cache.
  • Optional but impactful: a GPU or a host with AVX2 support — TensorFlow face detection runs noticeably faster.

Install Docker and the Compose Plugin

Debian / Ubuntu

sudo apt update
sudo apt install -y ca-certificates curl gnupg
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
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
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

Fedora / RHEL / Rocky

sudo dnf -y install dnf-plugins-core
sudo dnf config-manager --add-repo \
  https://download.docker.com/linux/fedora/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo systemctl enable --now docker

Arch

sudo pacman -S docker docker-compose
sudo systemctl enable --now docker

Add your user to the docker group so you can manage containers without sudo:

sudo usermod -aG docker $USER
newgrp docker

Plan Your Storage Layout

Getting this wrong is the single biggest source of pain when migrating PhotoPrism later. There are three distinct directories:

  • Originals (/photoprism/originals inside the container) — your actual photos. Mount a real host path here. PhotoPrism can treat this as read-only if you set PHOTOPRISM_READONLY.
  • Storage (/photoprism/storage) — database cache, sidecar JSON/XMP files, TensorFlow models, and the preview thumbnails. This directory grows; keep it on a fast drive.
  • Import (optional, /photoprism/import) — a drop folder; files placed here are moved or copied into originals on the next import run.

Never put originals and storage on the same bind mount — separating them lets you back up originals independently and replace the storage volume without touching your photos.

Write the Docker Compose File

Create a project directory and write the compose file:

mkdir -p ~/photoprism && cd ~/photoprism
cat > compose.yaml << 'EOF'
services:
  photoprism:
    image: photoprism/photoprism:latest
    restart: unless-stopped
    stop_grace_period: 10s
    security_opt:
      - seccomp:unconfined
      - apparmor:unconfined
    ports:
      - "2342:2342"
    environment:
      PHOTOPRISM_ADMIN_USER: "admin"
      PHOTOPRISM_ADMIN_PASSWORD: "changeme-strong-password"
      PHOTOPRISM_AUTH_MODE: "password"
      PHOTOPRISM_SITE_URL: "http://localhost:2342/"
      PHOTOPRISM_DISABLE_TLS: "true"
      PHOTOPRISM_DEFAULT_TLS: "false"
      PHOTOPRISM_ORIGINALS_LIMIT: 5000        # MB per upload; -1 = unlimited
      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"  # set true to disable AI features
      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: "false"
      PHOTOPRISM_UPLOAD_NSFW: "true"
      PHOTOPRISM_DATABASE_DRIVER: "mysql"
      PHOTOPRISM_DATABASE_SERVER: "mariadb:3306"
      PHOTOPRISM_DATABASE_NAME: "photoprism"
      PHOTOPRISM_DATABASE_USER: "photoprism"
      PHOTOPRISM_DATABASE_PASSWORD: "insecure-db-password"
      PHOTOPRISM_FACE_SIZE: 50
      PHOTOPRISM_FACE_SCORE: 9.0
      PHOTOPRISM_FACE_OVERLAP: 42
      PHOTOPRISM_FACE_CLUSTER_SIZE: 80
      PHOTOPRISM_FACE_CLUSTER_SCORE: 15
    volumes:
      - "/mnt/photos:/photoprism/originals"
      - "./storage:/photoprism/storage"
    working_dir: "/photoprism"
    depends_on:
      - mariadb

  mariadb:
    image: mariadb:11
    restart: unless-stopped
    stop_grace_period: 5s
    security_opt:
      - seccomp:unconfined
      - apparmor:unconfined
    command: --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"
      MARIADB_USER: "photoprism"
      MARIADB_PASSWORD: "insecure-db-password"
      MARIADB_ROOT_PASSWORD: "insecure-root-password"
EOF

Change every password, adjust /mnt/photos to your actual originals path, and set PHOTOPRISM_SITE_URL to the URL you will reach from a browser. If you plan to put a reverse proxy (nginx, Caddy, Traefik) in front, set that public URL here — PhotoPrism embeds it in share links.

Start PhotoPrism

docker compose up -d

Watch the logs until you see "Web server started":

docker compose logs -f photoprism

Output will vary; a successful start looks roughly like:

# photoprism-photoprism-1  | time="..." level=info msg="Web server started on 0.0.0.0:2342"

Open http://<your-host>:2342 and log in with the admin credentials you set.

Index Your Library

Indexing reads every file in originals, extracts EXIF metadata, generates thumbnails, runs the TensorFlow classifier, and writes face embeddings. For a large library, trigger it via the CLI so it survives a terminal disconnect:

docker compose exec photoprism photoprism index --cleanup

Flags worth knowing:

  • --cleanup — removes index entries for files no longer present.
  • --force — re-indexes already-indexed files (slow; use after upgrading).
  • --dry-run — reports what would happen without writing anything.

You can also trigger indexing from Library → Index in the web UI, but the CLI gives you live progress in the terminal.

Configure Faces

Face recognition in PhotoPrism is a two-phase process: detection (finding faces in images during indexing) and clustering (grouping similar faces so you can label them as people).

After initial indexing, run the face clustering pass:

docker compose exec photoprism photoprism faces index

Key environment variables that control quality versus recall:

  • PHOTOPRISM_FACE_SIZE — minimum face size in pixels (default 50). Increase to skip tiny or blurry faces.
  • PHOTOPRISM_FACE_SCORE — minimum quality score (0–100). Higher values mean fewer but more confident detections.
  • PHOTOPRISM_FACE_CLUSTER_SIZE — minimum number of face samples to form a cluster. Raise this if you are getting too many spurious one-photo clusters.

Once clustering finishes, go to People in the sidebar to assign names. PhotoPrism learns from your labels and improves match suggestions over time.

Enable Places (Reverse Geocoding)

PhotoPrism ships a local geocoding database derived from OpenStreetMap — no external API key required. It is bundled inside the container and activated automatically when GPS coordinates are present in EXIF data.

If photos are not showing locations, check two things:

  1. Confirm EXIF GPS data actually exists: exiftool yourphoto.jpg | grep GPS
  2. Make sure the storage volume is writable — PhotoPrism extracts the geodata archive there on first run.

To regenerate place metadata after the geodata is updated:

docker compose exec photoprism photoprism places update

Originals vs. Preview Storage

Thumbnails and preview JPEGs are generated on demand and cached in storage/cache/thumbnails/. PhotoPrism generates several sizes (from 224 px up to 7680 px for high-DPI displays) — this cache can grow to gigabytes for a large library.

If disk space is tight, limit preview resolution in Settings → Advanced → Preview Size, or set PHOTOPRISM_JPEG_QUALITY lower (70 is a good compromise). You can safely delete the entire storage/cache/ directory; PhotoPrism regenerates it on next access, at the cost of slower initial loads.

Do not delete the storage/sidecar/ directory. It contains JSON and XMP files that store labels, face assignments, and manual edits that are not in the image EXIF data.

Verify Everything Is Working

# Check container health
docker compose ps

# Confirm the database connection is live
docker compose exec photoprism photoprism status

# List indexed photo count
docker compose exec photoprism photoprism stats

The web UI should show your photos under Library → Browse and your faces under People. If the photo count is zero after indexing, check that the originals volume mount is correct and that files have read permissions for UID 1000 (the default PhotoPrism user).

Troubleshooting

  • Permission errors during indexing: PhotoPrism runs as UID 1000 by default. Either chown -R 1000:1000 /mnt/photos on the host, or set PHOTOPRISM_DISABLE_CHOWN: "true" and run the container as your own UID with the user: key in compose.
  • Face detection produces no results: Verify PHOTOPRISM_DISABLE_TENSORFLOW and PHOTOPRISM_DISABLE_FACES are both false. Check docker compose logs photoprism | grep -i tensor for TensorFlow errors — hosts without AVX2 will fail silently on some builds.
  • MariaDB keeps crashing: Increase --innodb-buffer-pool-size or reduce it if RAM is constrained. The default 512 MB is appropriate for most home servers.
  • Places not resolving: Run docker compose exec photoprism photoprism places update and check that the storage volume has several hundred MB free for the geodata archive.
  • Slow indexing: Indexing is intentionally throttled to avoid overwhelming the host. It will run faster if you set PHOTOPRISM_WORKERS to the number of physical CPU cores, but this will peg your CPU at 100% for the duration.
tested on:Ubuntu 24.04Debian 12Fedora 40Arch rolling

Frequently asked questions

Can I use SQLite instead of MariaDB?
PhotoPrism supports SQLite for small personal libraries, but the project officially recommends MariaDB for any library over a few thousand photos. SQLite struggles with concurrent indexing and can corrupt under heavy write load.
Will PhotoPrism modify or delete my original files?
By default, PhotoPrism does not alter originals. It writes sidecar JSON/XMP files alongside them. Set PHOTOPRISM_READONLY to true if you want to guarantee no changes are ever made to the originals directory.
How do I keep PhotoPrism updated?
Run docker compose pull to fetch the latest image, then docker compose up -d to recreate the container. PhotoPrism runs database migrations automatically on startup.
Does face recognition work without an internet connection?
Yes. TensorFlow models are bundled inside the container and the geodata for places ships with the image as well. After the initial Docker pull, PhotoPrism operates fully offline.
My photos show in the library but have the wrong date. How do I fix this?
PhotoPrism reads date from EXIF, file name patterns, and folder structure in that priority order. If EXIF dates are wrong, correct them with exiftool on the originals, then re-index with the --force flag.

Related guides