I finally stopped scattering my photos across random cloud drives and moved the whole library into a single place I actually control: a self-hosted Photoview instance running in a lightweight Docker container on my homelab.
Why Photoview
As someone who shoots a lot of landscape and wildlife work, I wanted a gallery that:
- Reads my existing folder structure instead of forcing a new one on me β each mounted folder simply becomes an album.
- Understands RAW files and EXIF so I can browse by camera, lens, date, and even location.
- Stays small and fast β a tidy container stack, not a heavyweight app.
Photoview ticks all three boxes. It scans a directory of originals, builds thumbnails into its own cache, and serves a clean, searchable web gallery.
The build
Photoview ships an official Docker Compose example, and thatβs exactly what I based my setup on. The stack is three small services:
photoviewβ the app itself (imagephotoview/photoview:2), exposed on port8000.mariadbβ the database (imagemariadb:lts).photoview-prepareβ a tiny one-shot helper that fixes permissions on the media-cache folder before the app starts, then exits.
Configuration lives in a .env file, so the compose file itself stays clean.
.env
HOST_PHOTOVIEW_LOCATION=/srv/photoview
HOST_PHOTOVIEW_MEDIA_ROOT=/srv/photos
PHOTOVIEW_DATABASE_DRIVER=mysql
MARIADB_DATABASE=photoview
MARIADB_USER=photoview
MARIADB_PASSWORD=change-me
MARIADB_ROOT_PASSWORD=change-me-too
docker-compose.yml
services:
photoview-prepare:
image: photoview/photoview:2
container_name: photoview-prepare
network_mode: "none"
user: root
entrypoint: []
command: /bin/bash -c "sleep 1 && chown -R photoview:photoview /home/photoview/media-cache"
cap_add:
- CHOWN
volumes:
- "${HOST_PHOTOVIEW_LOCATION}/storage:/home/photoview/media-cache"
photoview:
image: photoview/photoview:2
container_name: photoview
restart: unless-stopped
ports:
- "8000:80"
depends_on:
photoview-prepare:
condition: service_completed_successfully
mariadb:
condition: service_healthy
environment:
PHOTOVIEW_DATABASE_DRIVER: ${PHOTOVIEW_DATABASE_DRIVER}
PHOTOVIEW_MYSQL_URL: "${MARIADB_USER}:${MARIADB_PASSWORD}@tcp(photoview-mariadb)/${MARIADB_DATABASE}"
PHOTOVIEW_LISTEN_IP: "0.0.0.0"
volumes:
- "${HOST_PHOTOVIEW_LOCATION}/storage:/home/photoview/media-cache"
# Your photos, mounted read-only. This folder becomes the media root.
- "${HOST_PHOTOVIEW_MEDIA_ROOT}:/photos:ro"
mariadb:
image: mariadb:lts
container_name: photoview-mariadb
restart: unless-stopped
environment:
MARIADB_AUTO_UPGRADE: "1"
MARIADB_DATABASE: ${MARIADB_DATABASE}
MARIADB_USER: ${MARIADB_USER}
MARIADB_PASSWORD: ${MARIADB_PASSWORD}
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
volumes:
- "${HOST_PHOTOVIEW_LOCATION}/database/mariadb:/var/lib/mysql"
healthcheck:
test: healthcheck.sh --connect --innodb_initialized
interval: 1m
timeout: 5s
retries: 5
start_period: 3m
Bring it up, then open http://<host>:8000, create the first user on the init
page, and point it at the media root (/photos β the container path from the
bind mount):
docker compose up -d
The first scan takes a while as it reads EXIF and generates thumbnails; after that, new imports show up with a quick rescan.
What I like after living with it
- Itβs genuinely lightweight. The stack idles quietly and only works hard during a scan, which I can trigger on demand.
- Mounting originals read-only (
:ro) means Photoview can never touch my masters β it only reads. That matters when the files are irreplaceable. - Folders become albums, so my existing
landscape / wildlife / cityscapestructure just works without reorganising anything. - EXIF, timeline, places, and face browsing turn a flat archive into
something I actually enjoy flicking through. (The map/Places view needs a free
Mapbox token via
MAPBOX_TOKENβ optional.) - Itβs mine. No subscription, no third party deciding how my photos get compressed or who can see them.
Updating
Because I pin the major tag photoview/photoview:2, I get patches and features
within v2 without surprise breaking changes. Updating is two commands:
docker compose pull
docker compose up -d
Next steps
Iβm putting it behind a reverse proxy with TLS so I can reach my library from anywhere, and wiring the scan into a small automation so new imports appear without me thinking about it. If you host your own photos, Photoview is an easy recommendation β small container, real ownership.