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.
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 legacydocker-composebinary). - 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/originalsinside the container) — your actual photos. Mount a real host path here. PhotoPrism can treat this as read-only if you setPHOTOPRISM_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:
- Confirm EXIF GPS data actually exists:
exiftool yourphoto.jpg | grep GPS - Make sure the
storagevolume 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/photoson the host, or setPHOTOPRISM_DISABLE_CHOWN: "true"and run the container as your own UID with theuser:key in compose. - Face detection produces no results: Verify
PHOTOPRISM_DISABLE_TENSORFLOWandPHOTOPRISM_DISABLE_FACESare bothfalse. Checkdocker compose logs photoprism | grep -i tensorfor TensorFlow errors — hosts without AVX2 will fail silently on some builds. - MariaDB keeps crashing: Increase
--innodb-buffer-pool-sizeor 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 updateand 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_WORKERSto the number of physical CPU cores, but this will peg your CPU at 100% for the duration.
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
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.