Will your server keep up with indexing?
Yes, if it clears PhotoPrism's published floor with room to spare: indexing reads every picture, makes thumbnails and runs classification and face detection, so a big collection keeps the machine busy for a long time.
- Processor and memory: at least 2 cores and 3 GB of physical memory on a 64-bit system. Above that, the docs want memory to keep pace with the core count; we read that as roughly 1 GB per core.
- Swap: at least 4 GB, and no hard memory limit on the container. The docs warn that a limit can cause unexpected restarts when the indexer briefly needs extra memory for a large file.
- Camera RAW files and big panoramas can push past the minimum, so they may call for extra swap or RAM.
- Disk: local SSD storage, above all for database and cache files. The performance guide says hard disks manage only 5% of the reads per second an SSD does.
- Tiny machines: with a gigabyte of RAM or under, face recognition and RAW conversion are switched off.
A server that suits a photo archive
Renting a dedicated server gets you a whole physical computer that runs your library alone: all of its cores, memory and disks are yours, and you log in as root. One line pairs 16 cores and 16 to 128 GB of memory with 1.92 TB of NVMe-SSD storage, which suits PhotoPrism's appetite for cores, memory and fast local storage. If your originals outgrow one drive, a custom server lets you pick the disks and memory yourself, and NVMe servers explains the faster storage.
A ready server is handed over within 30 minutes of approval. Nobody meters your port, so moving years of pictures in brings no bandwidth bill. Rack servers, blades and two cartridge models (m510, m710x) include a console you can open yourself, which rescues you if a firewall change locks you out. You manage the server; we keep the hardware and the network running. The X-Gene ARM cartridge doesn't fit here, though: Ubuntu 16.04 and 18.04 are all it runs.
Up and running: firewall, folders and the compose file
- Install Ubuntu from the control panel, then Docker Engine (release 28 onward) together with its Compose plugin.
- Check your swap with
swapon --show. Under 4 GB? Runsudo fallocate -l 4G /swapfile, thensudo chmod 600 /swapfile,sudo mkswap /swapfileandsudo swapon /swapfile, and make it survive a reboot withecho '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab. - Run the block below: IPv6 rules on, SSH allowed, ufw on while nothing else listens yet, then the folders, the project's example file (locked down, as it will hold passwords) and a random password.
sudo sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
sudo ufw allow OpenSSH
sudo ufw --force enable
sudo mkdir -p /srv/photoprism/originals /srv/photoprism/storage /srv/photoprism/database
cd /srv/photoprism && sudo wget https://dl.photoprism.app/docker/compose.yaml
sudo chmod 600 /srv/photoprism/compose.yaml
openssl rand -base64 24
Open /srv/photoprism/compose.yaml in an editor and make these changes before the very first start, because two of them only take effect once:
- Your admin password: put a new random value in
PHOTOPRISM_ADMIN_PASSWORD(8 to 72 characters). After the first start, this setting no longer changes it. If your password contains a dollar sign, type it twice. - Database passwords: give
MARIADB_PASSWORDandPHOTOPRISM_DATABASE_PASSWORDthe same new value, andMARIADB_ROOT_PASSWORDa different one. MariaDB reads them only when it first creates the database. - Keep the web port private: in our setup the ports line becomes
127.0.0.1:2342:2342instead of2342:2342. With no address in front, Docker publishes a port on 0.0.0.0 and [::], meaning every IPv4 and IPv6 address the server has. That IPv4 traffic goes around ufw, and the same goes for IPv6 when Docker manages its own IPv6 networking. Docker Engine 28 or newer keeps a loopback-only mapping on the server itself (moby#45610). - Folders: swap
~/Pictures,./storageand./databasefor the absolute paths under/srv/photoprismyou just created. PhotoPrism's docs advise absolute paths so data survives when containers are recreated. - Leave for later: keep
PHOTOPRISM_SITE_URLashttp://localhost:2342/, and keeprestart: unless-stoppedcommented out until everything works, as the file's own comment suggests.
Make the library yours before anyone can see it
cd /srv/photoprism && sudo docker compose up -d brings up PhotoPrism and MariaDB. Nothing is public yet, so reach it through an SSH tunnel from your laptop, as yourself: ssh -N -L 2342:127.0.0.1:2342 you@server.example.com. While that window stays open, http://localhost:2342/ in your browser is your new library.
Log in with the username admin and the password you chose, then go to Settings, the Account tab, and Change Password, so the value written in the file is no longer the live one. Forgot it later? cd /srv/photoprism && sudo docker compose exec photoprism photoprism passwd admin sets a new one from the server. Then copy your pictures into /srv/photoprism/originals and start indexing under Library, Index.
Putting it online with HTTPS
On a public server, the project says, PhotoPrism belongs behind an HTTPS reverse proxy, or photos and passwords travel in clear text. Follow this order.
- Name first. Create a DNS A record for, say,
pics.example.orgpointing to your server (plus an AAAA record if you use IPv6), and wait until it answers. - Install and open the door.
sudo apt install -y certbot python3-certbot-nginx nginx, thensudo ufw allow 'Nginx Full'for ports 80 and 443. nginx is a normal program on the host, and ufw keeps IPv4 and IPv6 visitors away from it until that profile is allowed. - Then the certificate.
sudo certbot certonly --nginx -d pics.example.org. The first time, certbot asks three things: an email address for expiry and security warnings; whether you say yes to the terms of service of Let's Encrypt, Y or N, where only Y lets you continue; and whether the Electronic Frontier Foundation may have your address, also Y or N, which is up to you. - Then the configuration. Put the configuration shown below into a new file,
/etc/nginx/sites-available/photos.conf, symlink it into the sites-enabled folder withsudo ln -s, test withsudo nginx -tand apply withsudo systemctl reload nginx. - Finally tell PhotoPrism its address. Change
PHOTOPRISM_DISABLE_TLSto"true", so nginx alone handles certificates, andPHOTOPRISM_SITE_URLtohttps://pics.example.org/, and reruncd /srv/photoprism && sudo docker compose up -d. Once the library opens over HTTPS, remove the#in front ofrestart: unless-stoppedand run that same command again, so PhotoPrism comes back by itself after a reboot.
map $http_upgrade $photoprism_conn {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name pics.example.org;
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name pics.example.org;
ssl_certificate /etc/letsencrypt/live/pics.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/pics.example.org/privkey.pem;
client_max_body_size 512M;
proxy_buffering off;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
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 X-Forwarded-Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $photoprism_conn;
}
}
Most of this is the NGINX example from PhotoPrism's docs, including the Upgrade/Connection header pair that carries the WebSocket behind live indexing progress. What we adjusted: nginx talks to the private 127.0.0.1 port, the map has a name of its own, the HTTP redirect is placed in a location block so certificate renewals get through, and certbot's two include lines are left out. PhotoPrism accepts forwarded visitor addresses from Docker's usual internal range by default; we expect nginx's requests to reach it from there, and if yours do not, add that address to PHOTOPRISM_TRUSTED_PROXY.
Keeping every photo safe
A PhotoPrism backup, as its docs describe it, is your originals and an index database copy, and they recommend the storage folder as well, since it keeps your configuration, thumbnails and sidecar files. Copying the live MariaDB folder is not reliable, so dump it with PhotoPrism's own command and send everything to a second machine you control, such as a NAS. Cron runs as root, so the block gives root a key of its own (keeping any key root already has) and, as its very last line, copies it to the NAS.
sudo test -f /root/.ssh/id_ed25519 || sudo ssh-keygen -t ed25519 -N '' -f /root/.ssh/id_ed25519
sudo mkdir -m 700 -p /var/log/pp-backup
sudo tee /usr/local/sbin/pp-backup.sh > /dev/null <<'EOF'
set -euo pipefail
cd /srv/photoprism
docker compose exec -T photoprism photoprism backup -i -f
rsync -a compose.yaml storage originals you@nas.example.org:photoprism-copy/
EOF
printf '15 4 * * * root umask 077; bash /usr/local/sbin/pp-backup.sh > /var/log/pp-backup/run.log 2>&1\n' | sudo tee /etc/cron.d/pp-backup
sudo ssh-copy-id -i /root/.ssh/id_ed25519.pub you@nas.example.org
The dump lands in storage/backup/mysql/ named after the date, so it travels with the storage folder. ssh-copy-id is the interactive step: it asks you to confirm the NAS's host key (type yes) and then for your password on the NAS, once. ssh-copy-id sits last on purpose, so its questions cannot swallow other pasted lines. Then run sudo bash /usr/local/sbin/pp-backup.sh once to watch it finish. We leave out --delete, so a picture you remove by accident is still on the other side. Because the copied compose.yaml contains your passwords, keep that NAS private.
Rolling the library back
- Make sure the NAS really holds a recent dump:
sudo ssh you@nas.example.org 'ls -l photoprism-copy/storage/backup/mysql/'. - Keep a way back. While the app still runs, save today's index in a separately named dump:
cd /srv/photoprism && sudo docker compose exec photoprism photoprism backup -f -i /photoprism/storage/backup/before-restore.sql. - Stop the app:
cd /srv/photoprism && sudo docker compose stop photoprism. - Set the current storage folder aside with
sudo mv /srv/photoprism/storage /srv/photoprism/storage.old, and check thatsudo ls -l /srv/photoprism/storage.old/backup/before-restore.sqlshows a file with a non-zero size. If your originals were harmed too, set them aside the same way asoriginals.old. - Bring the copy home:
sudo rsync -a you@nas.example.org:photoprism-copy/storage /srv/photoprism/, plussudo rsync -a you@nas.example.org:photoprism-copy/originals /srv/photoprism/if the originals were set aside too. Confirm the dumps arrived withsudo ls -l /srv/photoprism/storage/backup/mysql/. If you setPHOTOPRISM_UID, give that user ownership of the restored folders with chown. - Start it again with
cd /srv/photoprism && sudo docker compose up -dand load the index withcd /srv/photoprism && sudo docker compose exec photoprism photoprism restore -f -i. With no file name it takes the most recent SQL dump in its backup directory; add a path such as/photoprism/storage/backup/mysql/2026-10-01.sqlto choose. - Sign in and look around; accounts and passwords are the ones from the dump's date.
- If something is wrong, go back: stop the app, rename the new folder with
sudo mv /srv/photoprism/storage /srv/photoprism/storage.failed, put the old one back withsudo mv /srv/photoprism/storage.old /srv/photoprism/storage(for originals you set aside, first rename the restored folder withsudo mv /srv/photoprism/originals /srv/photoprism/originals.failed, then bring the old one back withsudo mv /srv/photoprism/originals.old /srv/photoprism/originals), start it, and rerun the step 6 restore with/photoprism/storage/backup/before-restore.sqlas the file. - Only when everything checks out, delete the spare copies:
sudo rm -rf /srv/photoprism/storage.old, and, when it exists,/srv/photoprism/originals.old.
On a brand-new server, put the saved compose.yaml in /srv/photoprism before MariaDB starts even once: it creates its user from those passwords on the first start only.
Updating without surprises
New versions may upgrade the index database as they start, so keep a dump under a name of its own first: cd /srv/photoprism && sudo docker compose exec photoprism photoprism backup -i /photoprism/storage/backup/pre-upgrade.sql. After that, cd /srv/photoprism && sudo docker compose up -d --pull always pulls the new image and restarts. The database image is pinned to a version tag (mariadb:12.3 today); move to a new major version yourself after a backup, because MariaDB cannot be downgraded. Automatic updates with Watchtower are possible, but the docs warn they can interrupt indexing and imports.
What the licence and the editions mean for you
PhotoPrism's source code on GitHub is under the GNU AGPL 3.0, a copyleft licence. In plain words, and as our own reading rather than legal advice: whoever hands copies on to others, changed or unchanged, has to make the source available with them. Section 13 then reaches further than ordinary distribution: if you change the code and people use your changed version over a network, you must offer them its source as well. The usual Docker image, photoprism/photoprism:latest, is a different case: the project ships it under its Plus License, which lets the member extensions ship inside it, and offers a ce tag, a somewhat smaller AGPL-licensed image, for people who want none of them; system and third-party parts inside still carry their own terms.
The docs suggest starting on the free Community Edition; members switch extra features on from the admin account via the activation guide. A build from source has every Community Edition feature and most Essentials features, but not the additional user roles. One more rule from the licence page: you may not put the PhotoPrism trademark in the name of your own app or service, so call your library something of your own.
Questions people ask
- Do I have to pay to run PhotoPrism on my own server?
- No. The Community Edition is free, and the docs recommend new users install it before considering a membership. A membership adds features you switch on from the admin account.
- What username and password do I log in with the first time?
- The username is admin, and the password is the value you put in PHOTOPRISM_ADMIN_PASSWORD before the first start. The example file ships with the word insecure there, which you should replace; the docs note there is no fallback password when the setting is empty.
- Can it use a graphics card for face recognition?
- Yes, with an NVIDIA card in a server with a 64-bit Intel or AMD processor: swap the image for photoprism/photoprism:cuda and add the GPU device reservation the docs describe. Without a card, the AI models simply run on the processor.