PhotoStructure for Docker
- Read the pros and cons of Docker and Node editions
- What’s a βvolume?β (if you want your library portable
across machines, create
.uuidfiles) - PhotoStructure user guide
π₯οΈ System requirements
The docker images run under
linux/amd64 (x64) and linux/arm64 Docker hosts.
PhotoStructure containers require at least 2GB of RAM during imports. You’ll see
ENOMEM errors if you run within a 1GB container. PhotoStructure imports run
faster when more RAM and more CPU are allocated to the container.
The x64 docker image is built on a recent CPU with SSE4 and AVX extensions:
x64 CPUs that are less than a decade old should work fine, but if your server is
running an older processor (like an AMD Phenom II), the image may not spin up.
The arm64 image runs on Raspberry Pi 4 and later, as well as other arm64 hosts.
Docker Desktop (macOS/Windows) works but expect reduced I/O performance compared to native Linux Docker Engine due to the virtualization layer.
π¬ Video and HEIF/HEIC support
The base image doesn’t bundle FFmpeg or HEIF/HEIC tooling. On first run,
PhotoStructure asks whether to install FFmpeg, then fetches it from
conda-forge with Pixi. The same
FFmpeg reads .heic photos, so it covers both jobs.
Nothing runs as root. Everything lands in pixi/linux-<arch> inside your
system settings directory, owned by the user PhotoStructure already
runs as, so the container needs no elevated privileges. That is what makes this
work under hardened runtimes that set no-new-privileges, including every
TrueNAS SCALE app.
Most people don’t bind-mount /ps/config, so in practice that is usually
/ps/library/.photostructure/docker-config/pixi/linux-<arch>. If you set
PS_CONFIG_DIR, or you do bind-mount /ps/config, the tools follow it there.
Expect roughly 700 MB. Your settings directory is on a bind mount either way β
/ps/config if you mounted it, otherwise inside your library β so the tools
survive docker pull instead of being refetched after every image update. The
architecture suffix lets you move that directory between an arm64 host such as
a Raspberry Pi and an amd64 one: each installs its own copy. Set PIXI_HOME to
put them somewhere else entirely.
Your consent is stored as codec-install-consent.env, written both to your
settings directory and to /ps/library/.photostructure/. Delete those files,
or visit localhost:1787/welcome/tools, to reset it.
See media codec support for more information.
π Using docker run directly
The Compose Wizard is recommended because it
generates a working configuration with all bind mounts and permissions. If you
prefer docker run, here is a minimal example:
docker run \
--name photostructure \
--restart unless-stopped \
--publish 1787:1787 \
-e PUID=$(id -u) \
-e PGID=$(id -g) \
-e TZ=$(cat /etc/timezone 2>/dev/null || echo UTC) \
-v /path/to/library:/ps/library \
photostructure/server:beta
Replace /path/to/library with an actual directory path. To import photos from
other directories, add more -v flags (append :ro for read-only):
-v /mnt/photos:/photos:ro \
See bind-mount setup below for the full list of configurable directories.
ποΈ Docker bind-mount setup
PhotoStructure for Docker needs at least one bind-mount to store your library. The Docker Compose Wizard handles this for you. The reference below explains what each mount does.
PhotoStructure writes to up to 5 directories, all configurable. Only /ps/library is
required. The other /ps/... directories fall back to subdirectories within
your library if not mounted (thanks to “docker easy-mode”).
Your library directory (/ps/library)
- This is where your PhotoStructure Library will be stored.
- This bind-mount is required.
- It must be readable and writable by the PhotoStructure container, and have sufficient free space.
- If there is no
/ps/tmpbind-mount, this must be a local disk.
Changing where your library lives
On Docker, your library’s location is part of your container’s configuration, so PhotoStructure can’t change it from the settings page. The settings page shows you where your library is and links back here; to point PhotoStructure at a different folder, change the container and restart it.
Most people bind-mount their library to /ps/library and set nothing else.
There are two ways to change it:
- Point the
/ps/librarybind mount at a different folder on your host. This is what most people want. The path inside the container stays/ps/libraryβ you’re only changing which of your folders appears there. - Or set
PS_LIBRARY_DIRto a different path inside the container, and bind-mount your library folder to that path.PS_LIBRARY_PATHandPS_LIBRARYare accepted as older spellings. A blank value counts as not set, so adocker-compose.ymlwithPS_LIBRARY_DIR: ${PS_LIBRARY_DIR}and nothing set on the host behaves the same as leaving it out.
PS_LIBRARY_DIR always wins, like every other PhotoStructure environment
variable. When you finish setup PhotoStructure also saves the location to its
system settings, which is what it uses when the variable isn’t set β
so you can set the variable to override, or leave it unset and let the saved
value stand.
Originals directory (/ps/originals)
PhotoStructure always scans this directory for photos and videos to import. Mount your originals volume here to keep them separate from your library database and previews.
When Copy assets to library is enabled, PhotoStructure copies newly found
media from other locations into /ps/originals instead of your library
directory. This lets you keep originals on a large drive while the library
database and previews stay on a faster SSD. See the hybrid library forum
post
for more background.
If this bind-mount doesn’t exist, PhotoStructure stores copied assets in your library directory.
Note: If you open your library on another computer without this volume
mounted, full-screen zoom and non-transcoded video won’t work. You’ll need to
set originalsDir on each machine that accesses the library.
Local cache directory (/ps/tmp)
This is PhotoStructure’s cache directory, only required if /ps/library is stored on a network or remote drive.
/ps/tmpmust be on a local disk.- This volume should have at least 16-32 GB free.
- If you’ve got an SSD, use that: this directory will see a lot of reads and writes.
- If you can, pick (or create) a volume that doesn’t have data integrity protection, on-the-fly compression, or on-the-fly file de-duplication. This will make imports faster as well as reduce system load.
- PhotoStructure automatically prunes old and unused files from the scratch directory: it shouldn’t grow linearly with your library.
If this bind-mount doesn’t exist, PhotoStructure will use
/ps/library/.photostructure/cache instead.
Log file directory (/ps/logs)
This is where PhotoStructure stores logfiles. Normally PhotoStructure should be
mostly quiet, unless you set PS_LOG_LEVEL to info or debug.
PhotoStructure deletes log files older than 1 week automatically.
If this bind-mount doesn’t exist, PhotoStructure will use
/ps/library/.photostructure/logs instead.
System settings directory (/ps/config)
This is where PhotoStructure stores system settings, your codec-install consent, and the FFmpeg install itself.
PhotoStructure picks it in this order:
PS_CONFIG_DIR, if you set it./ps/config, if you bind-mounted it./ps/library/.photostructure/docker-configotherwise β this is what most people get, as the/ps/configbind mount is optional and uncommon.
A /ps/config bind mount is only worth setting up if you want to move a
library between physical servers while keeping per-machine settings separate.
ποΈ External directories to import
To add directories you want to import, add them as bind volumes to your container.
A root directory like
/photosor/albumsworks well.Don’t bind-mount into any previously-existing file or directory, like
/etc,/bin,/opt, or/usr.
- To prevent PhotoStructure from modifying imported directories, use read-only bind-mounts, with a couple caveats:
- PhotoStructure can’t write a
.uuidfile to read-only volumes, so you’ll need to create one manually. Without a.uuid, PhotoStructure can’t build stablepsfile://URIs, which means filesystem tags won’t work and your library won’t be portable across machines or container rebuilds. - PhotoStructure won’t be able to save metadata changes (rotation, edits) back to these files.
/ps/librarymust be read-writable, so bind-mount your photos and videos to a different directory.
- PhotoStructure can’t write a
π« Directories to omit
PhotoStructure will import all mounted directories recursively. PhotoStructure
respects NoMedia files, and will exclude those directories from being
imported. (learn more)
βοΈ Advanced settings
PhotoStructure has additional settings that are only configurable outside of the PhotoStructure UI.
Docker users may find it’s easiest to use environment variables to override settings. Read more about PhotoStructure’s use of environment variables.
π€ Running PhotoStructure for Docker as a non-root user
β οΈ Why not run as root?
Dockerized applications run as root by default. If PhotoStructure runs as root, the files in your library will be owned by root, which can cause permission issues with the original files that PhotoStructure copies into your library.
Running as non-root is more secure.
How do I specify the userid and groupid?
PhotoStructure uses linuxserver.io-style PUID/PGID environment
variables.
Default PUID/PGID values
If $PUID or $PGID is not set, PhotoStructure checks the current owner of
the system settings.toml or library settings.toml. If both are missing,
it defaults to 1000 for both, which is the userid and groupid assigned to
the first non-system user on Ubuntu and Fedora.
Run as root
If you want PhotoStructure to run as root, set PUID=0 and PGID=0.
Why is PhotoStructure running as node?
Rather than creating a new user in PhotoStructure’s docker container, we just
change the userid and groupid of node to match $PUID and $PGID.
chown on startup
PhotoStructure’s docker-entrypoint.sh script will chown -R your
library support directories if the current owner doesn’t match $PUID, so
that after we drop root privileges to be node, we still can read and write to your library.
Disable these chown calls by setting the environment variable PS_NO_PUID_CHOWN=1.
More PUID/PGID info
- Original PUID/PGID feature request
- Troubleshooting file permission issues from inconsistent PUID/PGID
π‘ Networking setup
Configuring the HTTP port
PhotoStructure listens on port 1787 by default. To use a different port:
- Set the
PS_HTTP_PORTenvironment variable - Update the
--publishport mapping to match
docker run \
-e PS_HTTP_PORT=8080 \
--publish 8080:8080 \
... # other options
photostructure/server:beta
Important: Both the environment variable (PS_HTTP_PORT) and the container
port in --publish must match. The host port (left side of :) can be
different if you want to expose PhotoStructure on a different external port.
By default, the Docker image listens on all interfaces inside the container, and
Docker publishes the port on all host interfaces when you use --publish 1787:1787. As of v2026.4.7, PhotoStructure has no built-in authentication, so
only do this on networks you trust.
To restrict access to the Docker host, bind the published port to
127.0.0.1:
docker run \
--publish 127.0.0.1:1787:1787 \
... # other options
photostructure/server:beta
Then open http://localhost:1787 on the host. Other devices on your LAN will not be able to reach PhotoStructure.
β οΈ Port conflict errors (EADDRINUSE)
If PhotoStructure cannot bind to its port, check docker logs photostructure for
the error message.
Common fixes:
- Stop old containers:
docker ps -a | grep photostructurethen stop/remove duplicates - Use a different port:
-e PS_HTTP_PORT=8080 --publish 8080:8080 - Check for conflicts: Ensure no other container uses the same host port
PhotoStructure exits cleanly on port conflicts, allowing Docker’s --restart
policy to retry after you fix the issue.
Reverse proxy configuration
If you set up a reverse HTTPS proxy via nginx and find that dynamic updates (like the progress status) aren’t showing up, add this to your nginx config:
proxy_http_version 1.1;
proxy_set_header Connection "";
Docker health check
PhotoStructure’s Docker image includes a built-in health check that pings the web
server every 30 seconds. The health check automatically uses PS_HTTP_PORT if
you’ve configured a custom port.
# Quick status (shows health in STATUS column)
docker ps
# Detailed health info (requires jq: https://jqlang.github.io/jq/)
docker inspect --format='{{json .State.Health}}' photostructure | jq
# Without jq - just show health status
docker inspect --format='{{.State.Health.Status}}' photostructure
Health check timing:
- Start period: 60 seconds (allows time for first-run initialization)
- Interval: 30 seconds
- Timeout: 10 seconds
- Retries: 3 failures before marking unhealthy
If the container shows as “unhealthy”, the web server isn’t responding. Check
docker logs photostructure for error details.
π§ Troubleshooting
If PhotoStructure doesn’t spin up, add these environment variables to see what’s happening:
environment:
- "PS_LOG_LEVEL=info"
- "PS_LOG_STDOUT=true"
Use PS_LOG_LEVEL=warn for less output, PS_LOG_LEVEL=debug for more.
Still stuck? Visit the forum or discord.
ποΈ Soft-delete support
As of v2024.4, PhotoStructure complies with the XDG trash standard.
If PhotoStructure can write to the top directory of a bind mount that you want to delete photos or videos from, PhotoStructure will handle soft-deletes automatically.
If PhotoStructure cannot create new directories to the top directory of a bind mount, you will need to do some initial setup before soft deletes on that volume will work properly.
For every bind mount you want PhotoStructure to be able to soft-delete, say, $volume:
Ensure there is a
$volume/.Trash-$uiddirectory, where$uidis the user id PhotoStructure runs as (typically1000on Ubuntu). PhotoStructure will attempt to create this directory with correct permissions if it doesn’t already exist.Alternatively, you can create a
$volume/.Trashdirectory. The permissions on these directories must allow all trashing users to write, and the sticky bit must be set (if the filesystem supports it), so PhotoStructure can create a$volume/.Trash/$uidsubdirectory with0700permissions.Ensure the user that PhotoStructure is running as can write to that directory
Ensure the user can write to the directory that the photo or video will be soft-deleted from
Where’s my trash can contents?
Files will only show up on a Linux desktop trashcan if the docker bind mount volume is mounted at the same depth: the XDG standard doesn’t look in mountpoint subdirectories for .Trash or .Trash-$uid
What about ~/.local/share/Trash?
PhotoStructure for Docker will not attempt to use $XDG_DATA_HOME/Trash unless the directory already exists.
This doesn’t comply with the XDG trash specification: it is a precaution against storing your soft-deleted files ephemerally in the docker container, and having a container upgrade unexpectedly hard-delete your trash can.
If you want to use this directory, bind-mount it with correct permissions into your PhotoStructure container under /home/node/.local/share/Trash, but know that soft-deletes will now require file copies rather than file moves, which will be substantially slower depending on volume transfer speeds.
What happens if PhotoStructure cannot soft-delete a file?
If PhotoStructure cannot move a file into a suitable .Trash directory, PhotoStructure will hard-delete the file. A sync report will describe what action was performed for every file.
How do I disable soft-deletes?
PhotoStructure will irreversibly delete photos and videos when you “empty trash” via the user interface if the PS_TRY_SOFT_DELETES setting is false.
Windows and macOS don’t comply with the Freedesktop trash standard, so running Docker on those platforms won’t support “restore” or “empty trash”. You’d need to find and delete $volume/.Trash-$UID manually. It’s easier to just set PS_TRY_SOFT_DELETES=false.
ποΈ Portainer instructions
Portainer CE is an open-source Docker management GUI, useful if you prefer not to run docker commands directly.
Installation instructions for Linux are here.
Here’s a video walkthrough from forum member @avdp (the Portainer UI may have changed since this was recorded):
Synology Docker instructions
PhotoStructure runs on x64 and arm64 Synology NAS devices running DSM 7.2 or
later. Install the Container Manager package via Main Menu > Package Center (it was called “Docker” in older DSM versions).
For a walkthrough with screenshots, see MK Library’s PhotoStructure for Synology guide. Community tips are in the Synology NAS Tips forum thread.
Install on Synology
Find your UID and GID. PhotoStructure should run as your normal DSM user so that library files have the same ownership as the rest of your photos. SSH in and run
id your-username, or checkControl Panel > User & Group. On Synology, the first user is typically UID1026and the defaultusersgroup is GID100.Alternatively, you can create a dedicated
photostructureuser and add it to theusersgroup (GID100). Since your primary account is also inusers, both accounts can access the same files. This limits what the container can reach without affecting your own access. See the TRaSH Guides Synology setup for a walkthrough of this approach.Create shared folders for the library and bind mounts using
Control Panel > Shared Folder. A common convention is/volume1/docker/photostructure. Make sure your user has read/write access.Generate your
compose.yamlwith the Docker Compose Wizard. Use the UID/GID from step 1 forPUID/PGID, and Synology-style paths (like/volume1/docker/photostructure) for the library directory.Create a Project in Container Manager:
- Open
Main Menu > Container Manager - Click the Project tab, then Create
- Name your project (e.g.,
photostructure) - Set the path to where you want the
compose.yamlstored - Paste the output from the Docker Compose Wizard into the editor
- Click Done to launch
- Open
Verify PhotoStructure is running at
http://YOUR-NAS-IP:1787.
Do not select “high privilege.” Give PhotoStructure at least 2GB of RAM.
Alternative: via SSH
If you prefer the command line, enable SSH on your
NAS,
copy your compose.yaml to the NAS, and run:
sudo docker compose up --detach
Upgrade on Synology
If your compose.yaml includes pull_policy: always (the default from the
Docker Compose Wizard):
- Open Container Manager β Project
- Select your project β Action β Stop
- Select your project β Action β Build
Container Manager pulls the newest image and recreates the container.
For SSH users, see the upgrade instructions.
What about Docker Volumes?
Docker supports volumes, which are “the preferred mechanism for persisting data generated by and used by Docker containers.”
Docker Volumes may offer slightly better I/O performance, but bind mounts give you the flexibility to move your library to another computer or switch container hosts (like Podman).
Podman support
Forum user iamphotocat submitted podman installation instructions – thanks!
Pidfiles
You can use a process ID file (“pidfile”) to control PhotoStructure instances.
Set the PS_PIDFILE environment variable to an absolute path that PhotoStructure
can write to. Good defaults:
/var/run/photostructure.pidfor single-instance hosts/var/run/user/$PUID/photostructure.pidif running multiple concurrent libraries with different user IDs
If the parent directory doesn’t exist, PhotoStructure will try to create it, but
it must be writable by the PUID/PGID user and group.
Once running, the file contains the main process ID. To shut down:
- Send
SIGINTorSIGTERMto that PID, or - Run
photostructure --stop --pidfile=/path/to/pidfile
The --stop command exits with 0 after graceful shutdown, or non-zero if
something went wrong. Make sure the pidfile paths match between start and stop.
Edit history
August 2026
- Documented how your library’s location is resolved: using a path other than
/ps/library
July 2026
- Codec install no longer uses
sudoandapt. FFmpeg is fetched into your system settings directory as the unprivileged runtime user, so it works underno-new-privileges
April 2026
- Video and HEIF/HEIC support is now configured on first run inside the app (was previously opt-in via the Docker Compose wizard’s Dockerfile build)
February 2026
- Moved Docker setup to interactive Docker Compose Wizard
- This page is now a reference for Docker concepts, bind mounts, and platform-specific instructions
- Removed patent-encumbered FFmpeg/HEIF from base Docker image
April 2024
- Added soft-delete instructions
November 2023
- Integrated Docker easy-mode
July 2022
- Added Portainer section

