Google Photos is convenient right up until you start thinking about what "convenient" actually costs you. Your entire photo library, indexed by an algorithm you don't control, sitting on servers you'll never see. A lot of homelab users hit that realization and go looking for an alternative. Immich usually comes up first these days, and it's a great tool, but it's not the only one. PhotoPrism has been around longer, it's lighter on resources, and if you've already got a Proxmox VE box humming away, it's a genuinely easy thing to add.
This tutorial walks you through building a Debian 13 LXC container on Proxmox VE, installing Docker inside it, and standing up PhotoPrism with a real MariaDB database behind it. No Docker experience required. You won't need a beefy GPU either — PhotoPrism's AI features run fine on CPU for a typical home library, they're just slower than on dedicated hardware.
What You Will Learn
By the time you finish this guide, you'll have a working PhotoPrism instance running in its own isolated container on your Proxmox VE host. Along the way you'll learn how to:
- Create an LXC container correctly sized for PhotoPrism, with the settings Docker actually needs to run inside it
- Install Docker Engine and the Compose plugin on Debian 13
- Write a docker-compose.yml file for PhotoPrism and MariaDB, and understand what each setting does
- Point PhotoPrism at your existing photo library and run your first index
- Recognize the handful of errors almost everyone runs into on their first attempt, and fix them
Set aside about 30 minutes for the setup itself. The first index of your photo library will take a lot longer than that — anywhere from a few minutes for a couple hundred photos to several hours for tens of thousands, depending on your CPU and whether you enable face recognition.
What Is This Feature?
PhotoPrism is an open-source, self-hosted app for organizing and browsing your photo and video collection. You point it at a folder full of images, it scans them, and it builds a searchable library with automatic tagging, basic face recognition, and a map view built from photo GPS data. It runs entirely on your own hardware.
It's built around a container image with two moving parts in a typical setup: the PhotoPrism app itself, and a MariaDB database that stores your library metadata — albums, tags, face data, and so on. Your actual photo files stay as plain files on disk the whole time. PhotoPrism reads and indexes them, it doesn't lock them away in some proprietary format, which matters a lot if you ever decide to switch tools later.
If you've already looked at running Immich on Proxmox VE, you'll notice PhotoPrism takes a simpler path. Immich splits its AI features into a separate machine-learning microservice and leans on Redis plus Postgres with a vector extension. PhotoPrism keeps things to two containers and one database engine. That simplicity is really the whole pitch: less to configure, less to go wrong, lower RAM overhead on a box that's already running a dozen other things.
Why Would You Use It?
The obvious reason is privacy — your photos never leave your network unless you decide to expose the service yourself. But there are practical reasons too. PhotoPrism doesn't require the mobile-first architecture Immich is built around, so if your main goal is organizing an existing archive of photos you already have on a NAS or an old hard drive, rather than auto-backing up a phone, PhotoPrism's import-and-index workflow fits that use case a little more naturally.
It's also noticeably lighter. A MariaDB container plus the PhotoPrism app will happily run in 4 GB of RAM for a moderate library, where Immich's full stack tends to want more headroom once the machine-learning service is active. If your Proxmox host is a small mini PC or an older machine you've repurposed, that difference matters.
None of this means PhotoPrism is strictly "better." Immich has a more polished mobile app experience for automatic phone backup. Pick based on what you're actually trying to do, not which project has more GitHub stars this month.
Prerequisites
Before you start, make sure you have:
- Proxmox VE 8.x or 9.x installed and reachable through the web interface (this guide was written against 9.2)
- At least 4 GB of RAM free for the container, 6 GB or more if your photo library is large or you plan to enable face recognition
- 20 GB of free space for the container's root disk, plus however much space your photo library actually needs — this can be on separate storage, which we'll cover below
- A Debian 13 (Trixie) LXC template downloaded in Proxmox — grab it from Datacenter → local → CT Templates → Templates if you don't already have it
- Basic comfort typing commands into a terminal. You don't need to know Docker already.
Step-by-Step Tutorial
Step 1: Create the LXC Container
In the Proxmox web interface, click Create CT in the top right. Work through the wizard with these settings:
- General: give it a hostname like
photoprism, set a root password, pick an unused VMID (this guide uses 115) - Template: select the Debian 13 template you downloaded
- Disk: 20 GB is a reasonable starting point for the container root disk — the actual photo library will live elsewhere, more on that shortly
- CPU: 2 cores is plenty to start
- Memory: 4096 MB, with at least 2048 MB of swap. PhotoPrism's own documentation specifically recommends configuring several gigabytes of swap, because indexing large RAW files or panoramas can spike memory usage in short bursts
- Network: attach to
vmbr0(or whichever bridge your VMs use) with DHCP, or set a static IP if that's how you manage your network
Leave "Unprivileged container" checked. There's no good reason to run this one privileged, and running Docker in an unprivileged container is well supported at this point — it just needs one extra setting, which is the next step.
Step 2: Enable Nesting
Docker needs to create its own container namespaces, cgroups, and network interfaces inside your LXC container. By default, Proxmox blocks nested containerization for security reasons, so you have to explicitly allow it.
After creating the container (don't start it yet), select it in the left panel, go to Options, then double-click Features. Check the Nesting box and click OK. You can also do this from the Proxmox shell instead of the GUI:
pct set 115 -features nesting=1,keyctl=1
The keyctl=1 part matters too — some Docker operations touch the kernel keyring, and without it you'll see permission errors that have nothing obviously to do with keys. Skip this step and Docker will either refuse to start or crash the moment you try to run a container, so it's worth double-checking before you move on.
Step 3: Start the Container and Install Docker
Start the container, then open its console (either the Console button in the web UI or pct enter 115 from the Proxmox shell). First, update the package list and grab a couple of tools Docker's installer needs:
apt update && apt install -y curl ca-certificates
Now run Docker's official install script. It detects your distribution and sets up the right repository automatically:
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
This installs Docker Engine along with the Compose plugin — you get the docker compose command (no hyphen; that's the newer plugin syntax, not the older standalone docker-compose binary). Confirm it worked:
docker --version
docker compose version
Both commands should print a version number. If either one comes back with "command not found," the install script didn't finish cleanly — scroll back up through its output for the actual error before doing anything else.
Step 4: Set Up the Directory Structure
Create a working directory for PhotoPrism's compose file and its persistent data:
mkdir -p /opt/photoprism/storage
mkdir -p /opt/photoprism/originals
cd /opt/photoprism
The originals folder is where your actual photos and videos will live — this is the folder PhotoPrism scans and indexes. The storage folder holds thumbnails, the search cache, sidecar files, and database backups. If you already have a photo archive sitting on a NAS or another storage device, this is the point where you'd mount it here instead of using a plain local folder — for example, as a bind mount point on the container pointing at an NFS or SMB share you've already set up in Proxmox.
Step 5: Write the Docker Compose File
Create /opt/photoprism/docker-compose.yml with the following content:
services:
photoprism:
image: photoprism/photoprism:latest
stop_grace_period: 15s
depends_on:
- mariadb
security_opt:
- seccomp:unconfined
- apparmor:unconfined
ports:
- "2342:2342"
environment:
PHOTOPRISM_ADMIN_USER: "admin"
PHOTOPRISM_ADMIN_PASSWORD: "changeme123"
PHOTOPRISM_SITE_URL: "http://192.168.1.50:2342/"
PHOTOPRISM_DATABASE_DRIVER: "mysql"
PHOTOPRISM_DATABASE_SERVER: "mariadb:3306"
PHOTOPRISM_DATABASE_NAME: "photoprism"
PHOTOPRISM_DATABASE_USER: "photoprism"
PHOTOPRISM_DATABASE_PASSWORD: "changeme123"
PHOTOPRISM_DISABLE_TLS: "false"
PHOTOPRISM_DEFAULT_LOCALE: "en"
volumes:
- "./originals:/photoprism/originals"
- "./storage:/photoprism/storage"
mariadb:
image: mariadb:12.3
restart: unless-stopped
stop_grace_period: 15s
security_opt:
- seccomp:unconfined
- apparmor:unconfined
command: --innodb-buffer-pool-size=256M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
volumes:
- "./database:/var/lib/mysql"
environment:
MARIADB_AUTO_UPGRADE: "1"
MARIADB_INITDB_SKIP_TZINFO: "1"
MARIADB_DATABASE: "photoprism"
MARIADB_USER: "photoprism"
MARIADB_PASSWORD: "changeme123"
MARIADB_ROOT_PASSWORD: "changeme123"
Swap 192.168.1.50 for your container's actual IP address, and change both instances of changeme123 to real passwords — one for the PhotoPrism admin account and one for the database user. They don't need to match each other.
Step 6: Launch the Stack
From inside /opt/photoprism, bring everything up:
docker compose up -d
Docker will pull both images the first time, which takes a few minutes depending on your connection. Watch the startup logs to make sure PhotoPrism connects to the database successfully:
docker compose logs -f photoprism
You're looking for a line that says the web server is listening on port 2342. Press Ctrl+C to stop following the logs once you see it — that doesn't stop the container, it just stops the log stream.
Step 7: Log In and Index Your Library
Open a browser and go to http://<container-ip>:2342. Log in with the admin username and password you set in the compose file. Copy some photos into /opt/photoprism/originals on the container (or into whatever storage you mounted there), then trigger the first index either from the web UI's Library menu or from the command line:
docker compose exec photoprism photoprism index
This is where the time goes. PhotoPrism reads every file, extracts EXIF data, generates thumbnails, and — if you haven't disabled it — runs image classification and face detection on each photo. Leave it running in the background; there's no need to babysit it.
Commands Explained
| Command | What It Does |
|---|---|
pct set 115 -features nesting=1,keyctl=1 | Allows the LXC container to run nested containers and access the kernel keyring, both required for Docker |
docker compose up -d | Creates and starts every service defined in the compose file, in the background (-d for detached) |
docker compose logs -f photoprism | Streams the PhotoPrism container's log output live, useful for confirming it started correctly |
docker compose exec photoprism photoprism index | Runs inside the running container and starts (or resumes) indexing the originals folder |
docker compose exec photoprism photoprism passwd admin | Resets the admin password after the fact — the PHOTOPRISM_ADMIN_PASSWORD variable only sets it on the very first startup |
docker compose down | Stops and removes the containers without touching your volumes — your photos and database are untouched |
docker compose pull && docker compose up -d | Pulls newer images and recreates the containers with them — this is how you update PhotoPrism |
Common Errors
Docker daemon won't start, or containers immediately exit. Almost always a missing nesting flag. Run pct config 115 from the Proxmox shell and check the features line includes nesting=1. If it doesn't, stop the container, add it with the command from Step 2, and start it again.
"Error response from daemon: connect: permission denied" or keyring-related errors. This is the missing keyctl=1 flag. Same fix as above — add it, then restart the container.
PhotoPrism logs show repeated "dial tcp mariadb:3306: connect: connection refused." MariaDB takes a few seconds longer to initialize than PhotoPrism does on first startup, and depends_on only controls start order, not readiness. PhotoPrism retries automatically and should connect within 10-20 seconds. If it never connects, double-check the database name, user, and password in both services match exactly.
Thumbnails or the storage folder show permission errors. This one trips up a lot of people coming from a VM-based setup. In an unprivileged LXC container, the UID that Docker uses inside its containers gets remapped to a different, unprivileged UID on the container's own filesystem. If you're bind-mounting a folder that was created with different ownership — say, a share you mounted with root-only permissions — PhotoPrism may not be able to write to it. Running chmod -R 755 on the mounted folder, or matching ownership more carefully, usually clears it up.
Troubleshooting
Start with what's actually running:
docker compose ps
If a container shows "Restarting" instead of "Up," it's crash-looping. Check its logs specifically:
docker compose logs photoprism
docker compose logs mariadb
If the web UI loads but feels frozen or times out during indexing, check memory pressure on the container itself:
free -h
If available memory is near zero and swap is maxed out, that's your answer — bump the container's RAM allocation in Proxmox (Hardware tab, no need to reinstall anything), or reduce PHOTOPRISM_ORIGINALS_LIMIT and index in smaller batches.
Can't reach port 2342 from another machine on your network at all? Check whether the Proxmox firewall is enabled on the container (Firewall tab in the container's settings) and confirm there isn't a rule blocking inbound traffic on that port.
Best Practices
Keep your photo library on separate storage from the container's root disk. If you cram tens of thousands of RAW files onto the same 20 GB disk as the operating system, you'll fill it fast and it's painful to resize later. A mount point backed by its own ZFS dataset or a network share gives you room to grow without touching the container itself.
Change both passwords in the compose file before you ever run docker compose up for the first time. The PHOTOPRISM_ADMIN_PASSWORD variable only takes effect on the very first startup — after that, changing it in the file does nothing, and you have to use the photoprism passwd command instead.
Back up the storage and database folders, not just your photos. Your originals are replaceable if you keep another copy elsewhere, but albums, face data, and tags only exist in that MariaDB volume. Include /opt/photoprism in whatever backup job you're already running against this container, whether that's vzdump or Proxmox Backup Server.
Don't expose port 2342 directly to the internet. If you want remote access, put it behind a reverse proxy with a real TLS certificate — Nginx Proxy Manager running in its own LXC container is a common and fairly painless way to do this, and it also lets you use a real domain name instead of an IP and port.
Pin the image version once you're happy with how things run. photoprism/photoprism:latest is convenient at first, but it means every docker compose pull can jump you to a new release without warning. Switching to a specific tag, like photoprism/photoprism:250602, gives you control over when you actually upgrade.
Frequently Asked Questions
Do I need a GPU for PhotoPrism?
No. Face detection and image classification run on CPU by default and work fine for personal libraries. A GPU speeds things up but isn't required for the setup in this guide.
Can I run PhotoPrism in a VM instead of an LXC container?
Yes, and it's simpler in some ways since you skip the nesting configuration entirely. The tradeoff is a VM uses more RAM and disk for the same workload, since it's running a full separate kernel rather than sharing the host's.
Will PhotoPrism auto-backup photos from my phone like Immich does?
Not directly. PhotoPrism has a mobile app for browsing your library, but it's not built around continuous phone backup the way Immich is. If automatic phone backup is your main goal, Immich is the better fit.
What happens to my original photos if I delete the container?
Nothing, as long as they're in the mounted originals folder and not inside the container's writable layer. Deleting the container only removes what Docker manages directly — your bind-mounted folders on the container's disk are untouched.
How much does indexing slow down if I enable face recognition?
Expect it to take noticeably longer on the first pass, sometimes two to three times as long depending on your CPU, since PhotoPrism runs an additional detection pass on every photo. It's a one-time cost — after the initial index, only new photos get scanned.
Conclusion
You've now got a self-hosted photo library running in an isolated LXC container, with your data staying on hardware you control. The setup here — two containers, a handful of environment variables, one nesting flag — is about as lean as a self-hosted app stack gets on Proxmox VE. From here, the natural next steps are mounting a larger storage volume for your full archive, putting a reverse proxy in front of it, and folding the container into whatever backup routine you already run for the rest of your homelab.