PhotoPrism is a self-hosted photo library that indexes your pictures, reads their EXIF metadata, detects faces and labels, and shows geotagged photos on a map, all without sending data to a third-party service. In this tutorial you will run PhotoPrism and MariaDB with Docker Compose on Ubuntu 24.04, put Nginx with a Let's Encrypt certificate in front of it, add photos to the library, and schedule indexing and database backups.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with at least 2 CPU cores and 4 GB of RAM. Indexing and face detection are CPU and memory intensive; with less RAM, add swap.
- Enough disk space for your photos plus roughly 20-30% extra for thumbnails and sidecar files.
- A non-root user with
sudoprivileges. - Docker Engine and the Docker Compose plugin installed. See How to install Docker on Linux.
- A domain name, referred to as
your_domain(for examplephotos.example.com), with a DNS A record pointing to your server's public IP.
Step 1 - Creating the project directory and secrets
Keep everything PhotoPrism needs under /opt/photoprism: the Compose file, the database files, the generated cache (storage) and your photos (originals).
sudo mkdir -p /opt/photoprism/{originals,import,storage,database}
cd /opt/photoprism
Generate random passwords for the PhotoPrism admin account and the database, and store them in a .env file that Docker Compose reads automatically:
sudo tee /opt/photoprism/.env > /dev/null <<EOF
PHOTOPRISM_ADMIN_PASSWORD=$(openssl rand -base64 18)
MARIADB_PASSWORD=$(openssl rand -hex 24)
MARIADB_ROOT_PASSWORD=$(openssl rand -hex 24)
EOF
sudo chmod 600 /opt/photoprism/.env
Print the file once and save the admin password in your password manager; you need it to log in:
sudo cat /opt/photoprism/.env
Step 2 - Writing the Docker Compose file
Create the Compose file:
sudo nano /opt/photoprism/compose.yaml
Paste the following configuration and replace your_domain:
services:
photoprism:
image: photoprism/photoprism:latest
restart: unless-stopped
depends_on:
- mariadb
security_opt:
- seccomp:unconfined
- apparmor:unconfined
ports:
- "127.0.0.1:2342:2342"
environment:
PHOTOPRISM_ADMIN_USER: "admin"
PHOTOPRISM_ADMIN_PASSWORD: "${PHOTOPRISM_ADMIN_PASSWORD}"
PHOTOPRISM_AUTH_MODE: "password"
PHOTOPRISM_SITE_URL: "https://your_domain/"
PHOTOPRISM_DISABLE_TLS: "true"
PHOTOPRISM_ORIGINALS_LIMIT: 5000
PHOTOPRISM_HTTP_COMPRESSION: "gzip"
PHOTOPRISM_LOG_LEVEL: "info"
PHOTOPRISM_DATABASE_DRIVER: "mysql"
PHOTOPRISM_DATABASE_SERVER: "mariadb:3306"
PHOTOPRISM_DATABASE_NAME: "photoprism"
PHOTOPRISM_DATABASE_USER: "photoprism"
PHOTOPRISM_DATABASE_PASSWORD: "${MARIADB_PASSWORD}"
working_dir: "/photoprism"
volumes:
- ./originals:/photoprism/originals
- ./import:/photoprism/import
- ./storage:/photoprism/storage
mariadb:
image: mariadb:11
restart: unless-stopped
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
environment:
MARIADB_AUTO_UPGRADE: "1"
MARIADB_INITDB_SKIP_TZINFO: "1"
MARIADB_DATABASE: "photoprism"
MARIADB_USER: "photoprism"
MARIADB_PASSWORD: "${MARIADB_PASSWORD}"
MARIADB_ROOT_PASSWORD: "${MARIADB_ROOT_PASSWORD}"
volumes:
- ./database:/var/lib/mysql
A few settings are worth explaining:
127.0.0.1:2342:2342publishes the web port only on the loopback interface. Ports published by Docker bypass UFW, so binding to localhost keeps PhotoPrism reachable only through Nginx.PHOTOPRISM_SITE_URLmust be the public HTTPS URL, otherwise share links and redirects point to the wrong address.PHOTOPRISM_DISABLE_TLSturns off PhotoPrism's built-in TLS because Nginx terminates HTTPS.PHOTOPRISM_ORIGINALS_LIMITis the maximum size of a single original file in MB.
Note
PHOTOPRISM_ADMIN_PASSWORDis only used to create the admin account on the first start. To change the password later, use the web interface orphotoprism passwd admininside the container.
Step 3 - Starting PhotoPrism
Pull the images and start both containers in the background:
cd /opt/photoprism
sudo docker compose up -d
The first start takes a minute or two while MariaDB initializes and PhotoPrism creates its tables. Follow the log and press Ctrl+C once it settles:
sudo docker compose logs -f photoprism
Check that both containers are running:
sudo docker compose ps
Then query the status endpoint on the loopback port:
curl -s http://127.0.0.1:2342/api/v1/status
{"status":"operational"}
Step 4 - Configuring Nginx and HTTPS
Install Nginx and Certbot from the Ubuntu repositories:
sudo apt update
sudo apt install nginx certbot python3-certbot-nginx
Create a server block for PhotoPrism:
sudo nano /etc/nginx/sites-available/photoprism
server {
listen 80;
listen [::]:80;
server_name your_domain;
client_max_body_size 500M;
location / {
proxy_pass http://127.0.0.1:2342;
proxy_http_version 1.1;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_read_timeout 600s;
}
}
The Upgrade and Connection headers are required because the PhotoPrism interface uses WebSockets for live updates. client_max_body_size allows large uploads from the browser.
Enable the site, test the configuration and reload Nginx:
sudo ln -s /etc/nginx/sites-available/photoprism /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Allow SSH, HTTP and HTTPS in UFW. Allow OpenSSH before enabling the firewall so you do not lock yourself out:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
Request a certificate. Certbot adds the TLS settings to the server block and redirects HTTP to HTTPS:
sudo certbot --nginx -d your_domain
Open https://your_domain in your browser and log in as admin with the password from /opt/photoprism/.env.
Step 5 - Adding photos and indexing the library
PhotoPrism works with two folders:
originalsis your library. PhotoPrism indexes files here in place and does not move them. Browser uploads also end up here.importis a drop folder. The import command moves files from it intooriginals, sorted into folders by date, and skips duplicates.
To add an existing collection, copy it into originals, for example with rsync:
sudo rsync -a --info=progress2 /path/to/your/photos/ /opt/photoprism/originals/
Then run the indexer. The first run reads metadata, generates thumbnails and runs label and face detection, so it can take hours on a large library:
cd /opt/photoprism
sudo docker compose exec photoprism photoprism index --cleanup
The --cleanup flag removes index entries for files that no longer exist. When the command returns, reload the web interface: your photos appear under Search and Library, and photos with GPS coordinates in their EXIF data appear under Places.
To use the drop folder instead, place files in /opt/photoprism/import and run:
sudo docker compose exec photoprism photoprism import
Both actions are also available in the web interface under Library.
Step 6 - Scheduling indexing and database backups
Files added to originals from outside the web interface (rsync, a sync client, a backup job) only appear after an index run. The database holds the index, albums, labels and people, so it needs its own backup.
Create the backup script first. It dumps the database with the root password stored inside the MariaDB container, compresses the dump and keeps 14 days:
sudo nano /usr/local/bin/photoprism-db-backup
#!/usr/bin/env bash
set -euo pipefail
backup_dir="/var/backups/photoprism"
mkdir -p "$backup_dir"
cd /opt/photoprism
docker compose exec -T mariadb sh -c 'mariadb-dump -u root -p"$MARIADB_ROOT_PASSWORD" --single-transaction photoprism' \
| gzip > "$backup_dir/photoprism-$(date +%F).sql.gz"
find "$backup_dir" -name 'photoprism-*.sql.gz' -mtime +14 -delete
Make it executable and run it once to test it:
sudo chmod 750 /usr/local/bin/photoprism-db-backup
sudo /usr/local/bin/photoprism-db-backup
ls -lh /var/backups/photoprism
-rw-r--r-- 1 root root 2.1M Sep 25 10:12 photoprism-2026-09-25.sql.gz
Now schedule both jobs with a cron file:
sudo nano /etc/cron.d/photoprism
# Index new and changed files every night at 03:00
0 3 * * * root cd /opt/photoprism && docker compose exec -T photoprism photoprism index --cleanup > /dev/null 2>&1
# Dump the database every night at 04:30
30 4 * * * root /usr/local/bin/photoprism-db-backup
Your photos themselves live in /opt/photoprism/originals. Include that folder and /var/backups/photoprism in your off-server backups.
Step 7 - Updating PhotoPrism
Run the backup script, then pull the new images and recreate the containers. PhotoPrism migrates its database schema on start:
sudo /usr/local/bin/photoprism-db-backup
cd /opt/photoprism
sudo docker compose pull
sudo docker compose up -d
Troubleshooting
Nginx returns 502 Bad Gateway. PhotoPrism is not listening yet or has stopped. Check sudo docker compose ps and sudo docker compose logs --tail 50 photoprism. Database connection errors right after the first start usually mean MariaDB is still initializing; PhotoPrism retries on its own.
Indexing is very slow or the container restarts. Check memory with sudo docker stats and look for out-of-memory kills with sudo dmesg | grep -i oom. Add RAM or swap. For a very large first run you can temporarily add PHOTOPRISM_DISABLE_FACES: "true" and PHOTOPRISM_DISABLE_CLASSIFICATION: "true", run sudo docker compose up -d, index, then remove both settings and index again.
New files do not show up. Files copied into originals from outside PhotoPrism only appear after an index run. Run the index command from Step 5 and read its output for unsupported or damaged files.
Uploads fail with "413 Request Entity Too Large". Raise client_max_body_size in the Nginx server block and reload Nginx.
Conclusion
PhotoPrism now runs with Docker Compose and MariaDB behind Nginx with HTTPS, indexes your library every night and keeps two weeks of database dumps. As next steps, install PhotoPrism as a progressive web app on your phone from the browser menu, sync photos from a desktop through PhotoPrism's WebDAV endpoint, and copy originals and the database dumps off the server with a tool such as restic.
