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 (image photoview/photoview:2), exposed on port 8000.
  • mariadb β€” the database (image mariadb: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 / cityscape structure 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.