Where Immich in Docker stores your photos
Immich splits originals, thumbnails, transcodes and the Postgres database across two host paths. Learn which folders are the only copy and how to move them.
Where Immich in Docker stores your photos
Immich in Docker keeps your photos in one host directory, the one you set as UPLOAD_LOCATION in .env, and its database in a second host directory, DB_DATA_LOCATION. No library data lives anywhere else on the host. Every asset you see in the web interface is a file under the first path plus a row under the second, and the two are only useful together.
That pair of paths is also the decision people regret. They go into .env on day one, usually as something short on the root filesystem, and three years later the root filesystem is full of photographs. Choosing well now costs nothing.
This post describes Immich v3.2.2, released on 15 September 2026, installed from the official Docker Compose file. The version matters: the layout inside the media folder has changed between releases, and the container-side path is now /data. So before you trust any path in any guide, including this one, read the volumes: lines in your own docker-compose.yml. If Immich is not running yet, set it up with a self-hosted Immich server as a Google Photos replacement first, then come back here before you commit to the paths.
The two lines that decide where everything goes
services:
immich-server:
volumes:
- ${UPLOAD_LOCATION}:/data
database:
volumes:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/dataBoth are bind mounts: a directory on the host attached to a fixed path inside a container. The server process only ever reads and writes /data. It has no idea where that is on your disk, which is the whole reason moving the library is possible at all. If you are new to the difference between this and a Docker-managed volume, bind mounts and named volumes fail in different ways and it is worth ten minutes.
Use absolute paths in .env, such as UPLOAD_LOCATION=/srv/immich/media. A relative path is resolved against the directory you run docker compose from, so running the same command from a different directory creates a second, empty library and Immich starts up looking blank.
IMMICH_MEDIA_LOCATION is a separate variable with a default of /data, and the documentation says you probably should not set it. It names the path inside the container. Pointing it at a host path breaks things. UPLOAD_LOCATION is the one you want.
The machine learning models live in a named volume called model-cache, not under either path. It is a download cache. Losing it costs bandwidth and one slow first run, nothing more.
What is inside the upload location
Immich creates a fixed set of folders under UPLOAD_LOCATION, each holding one kind of file, and most with a per-user subdirectory named after the user ID. For v3.2.2 the documented set is:
upload/<userID>: original files uploaded from the browser, the mobile app or the CLI. This is the only copy of those photos.library/<userID>: originals arranged by the storage template, when that feature is on. Also the only copy.thumbs/<userID>: generated previews and face thumbnails. Regenerable.encoded-video/<userID>: transcoded copies of your videos, made for browser playback. Regenerable.profile/<userID>: user avatar images. Tiny, and the documentation classes it as replaceable.backups/: the dumps written by Immich's own automatic database backup. Not regenerable, because it is the database.
Whether your originals sit in upload/ or library/ depends on the storage template, which is a setting under Administration in the web interface. With it off, originals stay in upload/<userID>. With it on, they are stored under library/<userID> following the pattern you chose. Do not guess which applies to you: open the setting, then run sudo du -sh /srv/immich/media/* on your own box and read which folder actually holds the bytes.
Each of these folders also contains a hidden file named .immich. Immich writes them as markers so it can tell the difference between an empty library and a volume that failed to mount. They matter when you copy things, because a copy that drops hidden files produces a server that will not start.
Which folders are the only copy, and which ones regenerate
This is the distinction that decides your backup plan. upload/, library/ and the database hold information that exists nowhere else. If they are gone, they are gone.
thumbs/ and encoded-video/ are derived data. Immich can rebuild both from the originals using the Generate Thumbnails and Transcode Videos jobs in Administration > Jobs, each run with the All option. So they are not worth the space in a backup, but they are not free to rebuild either: transcoding a large video library is hours of CPU time, and on a small VPS it is a lot more than hours. That is the actual trade, not "regenerable means worthless".
How much space they take varies wildly with how many videos you have, so measure rather than trust a figure. sudo du -sh on each folder gives you the split for your library in a few seconds, and that number is the one your backup plan should be built on. If the total surprises you, what Immich actually needs in RAM and disk covers where the growth comes from.
Where the Immich database lives, and why you cannot just copy it
DB_DATA_LOCATION is mounted at /var/lib/postgresql/data, which makes it a PostgreSQL data directory. Postgres (the relational database Immich stores all its metadata in) owns that directory completely. It holds the tables, the write-ahead log, and the on-disk state of every extension the image loads.
Two things follow from that. First, copying the directory while the container is running gives you a torn copy: files captured at different moments, a write-ahead log that does not match the data files, and a restore that either refuses to start or starts with missing rows. Second, the directory is tied to the exact Postgres build that wrote it. The official compose file pins the database image by digest, ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0, because Immich needs specific vector-search extensions for face and smart search. Drop that data directory next to a stock postgres:17 image and it will not open.
The documentation warns against restoring by copying DB_DATA_LOCATION by hand for exactly this reason. Take a logical dump instead, setting the two variables at the top to the database name and user in your own .env:
DB_DATABASE_NAME=immich
DB_USERNAME=postgres
docker exec -t immich_postgres pg_dump --clean --if-exists \
--dbname="$DB_DATABASE_NAME" --username="$DB_USERNAME" | gzip > \
"/path/to/backup/dump.sql.gz"Restoring goes back through psql, with one substitution that the Immich docs require because the dump sets an empty search_path:
DB_DATABASE_NAME=immich
DB_USERNAME=postgres
gunzip --stdout "/path/to/backup/dump.sql.gz" | sed \
"s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" | \
docker exec -i immich_postgres psql --dbname="$DB_DATABASE_NAME" \
--username="$DB_USERNAME" --single-transaction --set ON_ERROR_STOP=onRun the backup command on your own server and look at the size of the file it produces. It is metadata only, so it is small next to the photos, and it is the half of the system people forget. There is a longer walkthrough in backing up and restoring an Immich server end to end, including how to test a restore before you need one.
One more placement rule: keep DB_DATA_LOCATION on local disk. Postgres depends on fsync meaning the data really reached the device, and network filesystems are where that promise gets broken. A network mount for the media folder is a normal setup. A network mount for the database data directory is how people lose libraries.
Note also that Immich's built-in automatic backup writes into backups/, which is inside UPLOAD_LOCATION. It protects you against a bad upgrade or a wrong delete. It does not protect you against losing that disk, because the dump is on the disk. Copy it somewhere else, or take your own dump somewhere else.
Can I just move the folder?
For the media folder, yes, and the reason is worth understanding. The database records where each file sits relative to the media location. The container always sees that location as /data. So if you move the entire tree and repoint UPLOAD_LOCATION at the new place, every reference still resolves, because nothing changed from the server's point of view. What breaks references is rearranging things inside the tree.
For the database directory, no, not while anything is running. Either stop the whole stack and move the directory cold, so nothing is writing during the copy, or dump and restore into a fresh directory. There is no safe hot copy of a Postgres data directory with cp.
The safe order for moving a library onto a block volume
This is the sequence for moving media onto a block storage volume attached to your VPS or onto a bigger disk. Do it in this order.
- Mount the new volume, for example at
/mnt/photos, and add it to/etc/fstabso it comes back after a reboot. A media folder that vanishes on reboot is the most common cause of the mount error below. - Stop everything with
docker compose down. Copying a live library gives you a database that references files the copy does not contain. - Copy with
sudo rsync -aHAX --info=progress2 /srv/immich/media/ /mnt/photos/media/. The-aflag preserves ownership, permissions and timestamps, and it includes the hidden.immichfiles.-Hkeeps hardlinks and-Xkeeps extended attributes. Mind the trailing slashes. - Run the identical
rsynccommand a second time. It should report almost nothing transferred. That is your proof the first pass finished, which is cheaper than finding out later. - Change
UPLOAD_LOCATIONin.envto the new path. Change nothing else. - Start with
docker compose up -d, then watchdocker compose logs -f immich-serveruntil it is serving. - Verify from the browser: open a recent photo, play a video, scroll to the oldest month in the timeline, and check Administration > Jobs for failures.
- Only then delete the old directory. Keeping it one more week costs disk. Deleting it early costs photos.
If the server comes up complaining about a missing marker file, with a message of the shape ENOENT: no such file or directory, open 'upload/encoded-video/.immich', that is the mount check doing its job. It means the path is not mounted, is empty, or is not readable by the container, and it is not a database problem. Fix the mount or the ownership. Do not set IMMICH_IGNORE_MOUNT_CHECK_ERRORS=true to silence it, because the check exists to stop Immich writing a brand new empty library over the top of a missing mount. Ownership mismatches after a copy are common enough to be worth a look at how PUID and PGID decide which files a container can read.
Moving the library to another machine entirely is the same copy with a network in the middle, and the same rule about the database. Pushing media to a remote target is covered in backing up a NAS to a storage VPS, which applies equally when the source is a photo library rather than a NAS.
What to back up, and what is a waste of space
Back up four things: the database dump, upload/ and library/, the small profile/ folder, and your .env plus docker-compose.yml. Those last two files are three kilobytes that tell you which paths, which passwords and which image versions the working system used.
Skip thumbs/ and encoded-video/. Skip the model-cache volume. Skip backups/ if you are taking your own dump, because backing up a backup twice just doubles the storage bill.
sudo rsync -aHAX --delete \
--exclude 'thumbs/' --exclude 'encoded-video/' \
/srv/immich/media/ backup-host:/backups/immich-media/Back up the database first, then the files. The order is not arbitrary. If files are copied first, a photo uploaded between the two steps will be in the file copy but not in the database dump, which is a harmless orphan file. Reverse the order and the database can hold a row pointing at a file your backup never captured, which shows up as a broken asset after a restore.
Why you should not reorganise the upload tree by hand
Because the database stores the path of every asset, and nothing reconciles that path with the filesystem. Rename upload/ to photos/, or sort the files into your own year folders, and the rows still point at the old locations. Immich then serves broken assets for files that are sitting safely on disk, and there is no rescan to fix it, because this tree is not a scanned folder. It is Immich's own storage.
When you want a different on-disk layout, the supported route is the storage template. Change the pattern in Administration, then run the Storage Template Migration job. It moves the files and updates the database rows together, which is the part you cannot do with mv.
When you want to keep photos in a structure you control, that is a different feature: an external library, which Immich scans read-only and leaves in place. Use that for a folder you intend to manage yourself, and leave UPLOAD_LOCATION alone. The same split between app-owned and user-owned storage comes up in other self-hosted apps, and the comparison in where Nextcloud in Docker puts your files is a useful second data point, since Nextcloud does have a rescan command and Immich's upload tree does not.
FAQ
Where does Immich store photos on the host?
In the directory you set as UPLOAD_LOCATION in .env, which the official compose file bind-mounts to /data inside the server container. Under it, originals are in upload/<userID> or, with the storage template enabled, in library/<userID>. Thumbnails are in thumbs/, transcoded videos in encoded-video/, avatars in profile/, and automatic database dumps in backups/. The database itself is not there: it lives in DB_DATA_LOCATION, mounted at /var/lib/postgresql/data.
Can I move the Immich library to a bigger disk?
Yes. Mount the new disk and put it in /etc/fstab, run docker compose down, copy with sudo rsync -aHAX so hidden .immich files and ownership survive, run the same rsync again to confirm it finished, change UPLOAD_LOCATION in .env, then bring the stack back up. Verify a photo and a video in the browser before you delete the old copy. The database keeps paths relative to the media location, so the move is invisible to it.
Do I need to back up thumbs and encoded-video?
No. Immich rebuilds both from your originals with the Generate Thumbnails and Transcode Videos jobs, run with the All option in Administration > Jobs. Leaving them out of the backup can save a large share of the total, though how large depends on how much video you have, so measure with sudo du -sh rather than assuming. The cost of skipping them is CPU time after a restore, which on a small VPS can run for hours.
Why does Immich fail to start with an error about a .immich file?
Immich writes a hidden .immich marker in each media folder and checks for it at startup. An error of the form ENOENT: no such file or directory, open 'upload/encoded-video/.immich' means the folder is not mounted, is unreadable by the container, or is a fresh copy that lost its hidden files. Fix the mount or the ownership rather than setting IMMICH_IGNORE_MOUNT_CHECK_ERRORS=true, because the check is what stops an empty library being created over a missing mount.
Can I back up the Postgres data directory by copying it?
Not while the containers are running, and the Immich documentation warns against restoring that way at all. A copy taken from a live data directory is inconsistent, and the directory is tied to the exact Postgres build and vector extensions in the pinned database image. Use pg_dump as shown above, keep the compressed dump off the server, and restore it through psql with the documented search_path substitution.