This post is a step-by-step guide to deploying the Photoview photo gallery on Docker using PostgreSQL instead of the default MariaDB. I’ll walk you through how I solved two key “gotchas”: first, a cryptic no such file or directory error caused by a volume path change in newer PostgreSQL images, and second, the file permissions required to allow Photoview to successfully scan your media library.

I was recently on the hunt for a great, self-hosted photo gallery. My search led me to Photoview, a beautiful, filesystem-based application that’s perfect for a Docker setup.

The official docker-compose.example.yml is fantastic and provides options for SQLite, MariaDB, and PostgreSQL. While MariaDB is the default, I’m a fan of PostgreSQL and wanted to use it for its performance and robustness. I decided to build my stack using photoview-prepare, photoview, and postgres:18-alpine.

This is the story of the cryptic error I hit, the rabbit hole I fell into, and the two key “gotchas” I had to solve to get it working.

The Plan: Modifying the YAML

My first step was to streamline the official YAML file for my specific stack:

  1. Remove the mariadb service.
  2. Remove the watchtower service (I’ll update manually).
  3. Enable the postgres service.
  4. Update the photoview service’s depends_on and environment sections to point to my new postgres service.

My plan was set. I created my .env file, finalized my docker-compose.yaml, and ran the magic command:

docker compose up -d

Gotcha #1: The “No Such File” Error

Almost immediately, it failed. The photoview-pgsql container wouldn’t start.

Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: error mounting "/path/on/my/host/database/postgres" to rootfs at "/var/lib/postgresql/data": change mount propagation through procfd: open o\_path procfd: open /var/lib/docker/overlay2/..../merged/var/lib/postgresql/data: no such file or directory: unknown

The error seemed obvious: no such file or directory.

“Stupid me,” I thought. “I forgot to create the host directory for the database volume.”

So, I ran mkdir -p /path/on/my/host/database/postgres and re-ran docker compose up -d

…And I got the exact same error.

This is where the real troubleshooting began. I ran ls -al to confirm the directory was there (it was). I checked its permissions. I started wondering if it was an SELinux issue, an AppArmor issue, or some weirdness with my external ExosStorage drive.

The “Aha!” Moment: RTFM (Read The Friendly Manual)

After chasing my tail, I went back to the Photoview GitHub page and started reading the README.md very carefully. And then I saw it. A critical note for PostgreSQL users:

ATTENTION to PostgreSQL users !!!

Please pay attention to the DB volume mount point change inside the
container: starting from version 18, the DB is mounted to the /var/lib/postgresql, while in earlier versions it was the /var/lib/postgresql/data.
The error was misleading. The no such file or directory wasn’t on my host machine. It was inside the container.
The postgres:18-alpine image (and postgres:16 and postgres:17, it turns out) no longer has a /var/lib/postgresql/data directory. The data path is now simply /var/lib/postgresql.

My error was in my docker-compose.yaml volume definition.

The Fix:

I changed this line in my postgres service definition:
From (Old/Wrong):

volumes:  
  - "${HOST_PHOTOVIEW_LOCATION}/database/postgres:/var/lib/postgresql/data"

To (New/Correct):

volumes:  
  - "${HOST_PHOTOVIEW_LOCATION}/database/postgres:/var/lib/postgresql"

I ran docker compose up -d, and… success! All services started.

Gotcha #2: The Scan Fails (Permissions)

I logged into the Photoview UI, completed the setup, and pointed it to my /photos directory. I hit the “Scan” button, and… nothing. The logs were filled with permission denied errors.

This is the second classic Docker “gotcha”. The photoview container doesn’t run as the root user. It runs as a non-root user (in its case, photoview, which often has a UID like 999 or 1000).

My media folder on the host, however, was owned by my personal user or root. The photoview container had no permission to read the files.

The Fix:

To fix this, I had to change the ownership and permissions of my media directory on the host machine.

First, I gave the container user (I’m using 999:999 as an example) ownership of the media files:

# Change 999:999 to the correct UID/GID if needed  
sudo chown -R 999:999 /path/to/my/photos

For good measure, I also ensured the permissions were readable (and writable for directories):

# WARNING: 777 is the "nuke" option and generally bad practice  
# for security. But for a local, trusted Docker setup, it's the  
# fastest way to solve the problem.  
sudo chmod -R 777 /path/to/my/photos

A more secure chmod might be sudo find /path/to/my/photos -type d -exec chmod 755 {} \; and sudo find /path/to/my/photos -type f -exec chmod 644 {} \;, but the chown is the most critical part.

After fixing the media permissions, I restarted the scan, and it worked perfectly.

Takeaways

  1. “No Such File” can be misleading. Always check both sides of a Docker volume mount (host path and container path).
  2. Read the documentation! Especially for new major versions. A one-line change in the README was the key to the whole problem.
  3. Permissions are always the second boss. If the container runs, but the app fails to read files, it’s almost always a UID/GID mismatch between your host files and the container’s non-root user.

My Final (Placeholder) Configuration

As promised, here are the templates I used. You’ll need to fill in the variables in the .env file.
docker-compose.yaml

services:
  ## Makes sure that the media cache folder is created with the correct permissions
  photoview-prepare:
    image: photoview/photoview:2.4.0
    hostname: photoview-prepare
    container_name: photoview-prepare
    user: root
    entrypoint: []
    command: /bin/bash -c "sleep 1 && chown -R photoview:photoview /home/photoview/media-cache"
    cap_add:
      - CHOWN
    volumes:
      - "/etc/localtime:/etc/localtime:ro" ## use local time from host
      - "/etc/timezone:/etc/timezone:ro"   ## use timezone from host
      - "${HOST_PHOTOVIEW_LOCATION}/storage:/home/photoview/media-cache"

  photoview:
    image: photoview/photoview:2.4.0
    hostname: photoview
    container_name: photoview
    restart: unless-stopped
    stop_grace_period: 10s
    ports:
      - "8000:80" ## HTTP port (host:container)
    ## This ensures that DB is initialized and ready for connections.
    depends_on:
      photoview-prepare:
        condition: service_completed_successfully
      postgres:
        condition: service_healthy
    ## Security options for some restricted systems
    security_opt:
      - seccomp:unconfined
      - apparmor:unconfined
    environment:
      ## IMPORTANT: Make sure to set PHOTOVIEW_DATABASE_DRIVER=postgres in your .env file
      PHOTOVIEW_DATABASE_DRIVER: ${PHOTOVIEW_DATABASE_DRIVER}
      
      ## PostgreSQL URL (Enabled)
      PHOTOVIEW_POSTGRES_URL: postgres://${PGSQL_USER}:${PGSQL_PASSWORD}@photoview-pgsql:5432/${PGSQL_DATABASE}?sslmode=${PGSQL_SSL_MODE}
      
      PHOTOVIEW_LISTEN_IP: "0.0.0.0"
      ## Uncomment the next variable and set a different value to change the port photoview uses inside the container.
      # PHOTOVIEW_LISTEN_PORT: 80
      
      ## Uncomment the next variable and set a different value to set the location of the media cache inside the container.
      # PHOTOVIEW_MEDIA_CACHE: "/home/photoview/media-cache"
      
      ## Optional: If you are using Samba/CIFS-Share and experience problems with "directory not found"
      # - GODEBUG=asyncpreemptoff=1
      
      ## If you want to use it, set the correct value in the .env file.
      PHOTOVIEW_VIDEO_HARDWARE_ACCELERATION: ${PHOTOVIEW_VIDEO_HARDWARE_ACCELERATION}
      
    ## Share hardware devices with FFmpeg (optional):
    # devices:
      ## Uncomment next devices mappings if they are available in your host system
      ## Intel QSV
      # - "/dev/dri:/dev/dri"
      ## Nvidia CUDA
      # - "/dev/nvidia0:/dev/nvidia0"
      # - "/dev/nvidiactl:/dev/nvidiactl"
      # - "/dev/nvidia-modeset:/dev/nvidia-modeset"
    volumes:
      - "/etc/localtime:/etc/localtime:ro" ## use local time from host
      - "/etc/timezone:/etc/timezone:ro"   ## use timezone from host
      ## SQLite volume (Commented out)
      # - "${HOST_PHOTOVIEW_LOCATION}/database:/home/photoview/database"
      - "${HOST_PHOTOVIEW_LOCATION}/storage:/home/photoview/media-cache"
      ## This is the path to your photos
      - "${HOST_PHOTOVIEW_MEDIA_ROOT}:/photos:ro"
      ## *Additional* media folders can be mounted like this (set the variable in .env file)
      # - "${HOST_PHOTOVIEW_MEDIA_FAMILY}:/photos/Family:ro"

  ## The postgres service, enabled instead of mariadb
  postgres:
    image: postgres:18-alpine
    hostname: photoview-pgsql
    container_name: photoview-pgsql
    restart: unless-stopped
    stop_grace_period: 5s
    ## Security options for some restricted systems
    security_opt:
      - seccomp:unconfined
      - apparmor:unconfined
    ## Uncomment the following 2 lines if you want to access the database directly
    # ports:
      # - "5432:5432" ## host:container
    environment:
      POSTGRES_DB: ${PGSQL_DATABASE}
      POSTGRES_USER: ${PGSQL_USER}
      POSTGRES_PASSWORD: ${PGSQL_PASSWORD}
      ## See other optional variables in the https://hub.docker.com/_/postgres
    volumes:
      - "/etc/localtime:/etc/localtime:ro" ## use local time from host
      - "/etc/timezone:/etc/timezone:ro"   ## use timezone from host
      ## Use /var/lib/postgresql for standard PostgreSQL data persistence
      - "${HOST_PHOTOVIEW_LOCATION}/database/postgres:/var/lib/postgresql" ## DO NOT REMOVE
    healthcheck:
      test: pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB
      interval: 1m
      timeout: 5s
      retries: 5
      start_period: 3m

.env

##================***================##
## These are the environment setup variables.
## Start setting up your instance from here.
## Syntax of the .env file is next:
## VARIABLE_NAME=variable value with everything after the '=' and till the end of the line.
## The variables with values, set in the docker-compose.yml directly, are for advanced configuration.
##================***================##

##----------Host variables-----------##
## This is the current folder, where all Photoview files and folders (except of your media library) are located
HOST_PHOTOVIEW_LOCATION=/mnt/ExosStorage/ServiceData/photoview

## This is where your original photos and videos located.
## Provide here the path to single root folder for your media collection.
HOST_PHOTOVIEW_MEDIA_ROOT=/mnt/ExosStorage/mypics
## If you'd like to map multiple folders from different locations, create additional variables
## here like the next one and modify the docker-compose.yml to match them and use in volume mappings.
# HOST_PHOTOVIEW_MEDIA_FAMILY=/full/path/to/folder

## This is where the Photoview data will be backed up
HOST_PHOTOVIEW_BACKUP=/mnt/ExosStorage/ServiceData/photoview_backup
##-----------------------------------##

##-------Photoview variables---------##
## PHOTOVIEW_DATABASE_DRIVER could have one of values: `mysql` (default), `sqlite`, `postgres`
PHOTOVIEW_DATABASE_DRIVER=postgres

## Optional: To enable map related features, you need to create a mapbox token.
## A token can be generated for free here https://account.mapbox.com/access-tokens/
## It's a good idea to limit the scope of the token to your own domain, to prevent others from using it.
# MAPBOX_TOKEN=yourToken
##-----------------------------------##

##----------Video variables----------##
## Set the hardware acceleration when encoding videos.
## Support `qsv`, `vaapi`, `nvenc`.
## Only `qsv` is verified with `/dev/dri` devices.
# PHOTOVIEW_VIDEO_HARDWARE_ACCELERATION=
##-----------------------------------##

##-------PostgreSQL variables--------##
## Uncomment the next lines if PHOTOVIEW_DATABASE_DRIVER is `postgres`
PGSQL_DATABASE=photoview
PGSQL_USER=photoview
## Note: If your `PGSQL_PASSWORD` contains special characters (e.g. `@`), make sure to URL-encode them.
PGSQL_PASSWORD=some_very_secure_password
## See https://www.postgresql.org/docs/current/libpq-ssl.html for possible ssl modes
# PGSQL_SSL_MODE=prefer
##-----------------------------------##

##-------Watchtower variables--------##
## The POLL_INTERVAL in sec
#WATCHTOWER_POLL_INTERVAL=86400
#WATCHTOWER_TIMEOUT=30s
#WATCHTOWER_CLEANUP=true
##\\\\\\\\\\\\\\\\\//////////////////##