Install Immich on TrueNAS with GPU acceleration, and migrate a library without losing faces or albums
Your photos are not the valuable part - every named face, album and person lives in the Postgres database. A complete build with the library on HDD and the database on NVMe, Intel GPU acceleration that is verified rather than assumed, and a migration that keeps everything you built.
Before you start
- TrueNAS SCALE with an NVMe pool and an HDD pool
- Docker available on the host
- An Intel iGPU if you want machine learning acceleration
- For a migration: access to the old system, and enough space for the library on the new one
Your values
Fill these in and every command below updates to match. Nothing is sent anywhere.
Run this at your own risk. These steps worked on my hardware; yours may differ. Take a backup first, read each command before running it, and see the disclaimer.
Immich is four containers, not one, and they want completely different things from your storage. Get that split right at the start and everything afterwards is easier - including the migration, which is the part most people get wrong.
The short version of the hard-won lesson: your photos are not the valuable part. Every face you have named, every album, every person and every place is in the Postgres database. Copy the photos to a new server and let Immich re-import them, and you keep the pictures and lose everything you did with them.
What the four parts are, and what each one needs
| Container | What it does | Where it belongs |
|---|---|---|
immich_server |
Web interface, uploads, the API | Library on bulk storage |
immich_postgres |
Faces, albums, people, search vectors | Fast storage. This is the crown jewels. |
immich_machine_learning |
Face detection, smart search, OCR | Model cache on fast storage |
immich_redis |
Job queue and cache | Nothing to persist |
The database does far more random IO than a media server's does. Every smart search and every face match hits a vector index, so putting it on spinning disks makes the whole interface feel slow. Redis is a cache and deliberately has no volume at all - if it is lost, jobs re-queue.
Plan the storage
zfs create -p nvme/databases/immich
zfs create -p nvme/apps/immich/model-cache
zfs create -p tank/ImmichThree datasets, three jobs:
{{NVME_POOL_PATH}}/databases/immich- the Postgres data directory{{NVME_POOL_PATH}}/apps/immich/model-cache- the downloaded ML models, a few GB, read constantly during scanning{{HDD_POOL_PATH}}/Immich- the photo library, thumbnails and encoded video
Separating the database onto its own dataset matters beyond speed: you can snapshot it on its own schedule, and a database snapshot is small and quick while a library snapshot is neither.
Configure it
Immich is configured by a .env file next to the compose file. The paths are the
important part.
mkdir -p /mnt/nvme/apps/immich
cd /mnt/nvme/apps/immich
cat > .env <<'EOF'
# Where the photo library lives - bulk storage
UPLOAD_LOCATION=/mnt/tank/Immich
# Where the Postgres data directory lives - fast storage
DB_DATA_LOCATION=/mnt/nvme/databases/immich
# Pin the version. Never run :release in production and wonder why it changed.
IMMICH_VERSION=v3.1.0
TZ=Etc/UTC
DB_PASSWORD=CHANGEME
DB_USERNAME=postgres
DB_DATABASE_NAME=immich
EOFChange DB_PASSWORD before you start anything. It is only used between the
containers, but it ends up written into the database on first run, and changing
it afterwards is more annoying than choosing it now.
Enable GPU acceleration properly
Machine learning on the CPU works, and it is slow. On a library of any size, face detection and smart search indexing take days rather than hours. An Intel iGPU with OpenVINO changes that.
Two things are needed and both must be true. This is where people lose time, because getting one of them right looks exactly like getting both right until you check.
First, the machine learning image must be the OpenVINO variant - note the suffix:
ghcr.io/immich-app/immich-machine-learning:v3.1.0-openvinoSecond, the container must actually be able to reach the GPU. Immich ships a
hwaccel.ml.yml file for this; the openvino section maps the render device and
sets the cgroup rule that permits it:
cd /mnt/nvme/apps/immich
grep -A 8 'openvino:' hwaccel.ml.ymlopenvino:
device_cgroup_rules:
- 'c 189:* rmw'
devices:
- /dev/dri:/dev/dri
volumes:
- /dev/bus/usb:/dev/bus/usbIn your docker-compose.yml, the machine learning service extends that section:
immich-machine-learning:
extends:
file: hwaccel.ml.yml
service: openvinoVerify the GPU is actually reachable
Do not skip this. A missing device does not produce an error - it produces a silent fallback to the CPU, and everything keeps working, just slowly, forever.
docker exec immich_machine_learning ls -l /dev/dricrw-rw---- 1 root video 226, 0 card0
crw-rw---- 1 root 107 226, 128 renderD128If that says "No such file or directory", the GPU is not passed through and no amount of configuration elsewhere will help.
Verify OpenVINO is actually being used
The device being present is necessary but not sufficient. Check the logs:
docker logs immich_machine_learning 2>&1 | grep -i -E 'openvino|provider' | tail -5INFO Setting execution providers to ['OpenVINOExecutionProvider',
'CPUExecutionProvider'], in descending order of priorityOpenVINOExecutionProvider listed first is what you want. CPUExecutionProvider
appearing after it is the fallback, which is correct - it should be there. If
CPU is the only entry, OpenVINO failed to initialise.
Migrating an existing library
Here is the part worth reading twice.
What lives where. The image files are on disk. Everything else - the people you named, the albums you built, the face clusters, the smart search embeddings, the shared links, the users - is in Postgres. Move the photos alone and Immich will happily re-import them and rebuild face detection from scratch, with none of your names attached.
So the database is the migration. The photos are just bytes that come along.
Route one: restore a database dump. This is the safer path and the one to choose if the two systems differ in any way.
Immich takes its own dump every night. Find it on the old system:
ls -la /mnt/tank/Immich/backups/immich-db-backup-20260913T020000-v3.1.0-pg14.19.sql.gzThe filename records the Immich version and the Postgres version, which is exactly what you need to know before restoring it somewhere else.
Copy the photo library and that dump to the new machine, bring up the new stack so it creates an empty database, then stop the server and machine learning containers, leaving Postgres running, and restore into it. Immich's own documentation carries the current restore command for your version - use theirs rather than a copied one, because it changes between releases.
Route two: copy the Postgres data directory. Faster, no restore time, and strictly conditional.
This only works if the Postgres major version and the vector extension versions are identical on both sides. Immich does not use stock Postgres; it uses a build with VectorChord and pgvecto.rs compiled in. A mismatch does not degrade gracefully - the database refuses to start.
If you take this route, pin the image by digest rather than by tag so the two ends cannot drift:
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:...Stop Immich completely on both ends before copying. A Postgres data directory copied while running is not a database, it is a corrupted one.
docker compose down
rsync -aHAX --info=progress2 \
old-nas.example.lan:/path/to/old/postgres/data/ \
/mnt/nvme/databases/immich/Either route: keep the library path consistent. Immich stores paths. If
UPLOAD_LOCATION pointed somewhere else on the old system, the database will
reference files that are not where it expects them.
External libraries: photos that stay where they are
Immich can index a folder in place rather than importing it. Your existing photo folders keep their structure, keep being written to by whatever put them there, and still appear in Immich with faces and search.
Mount the folder into the server container:
volumes:
- /mnt/tank/Immich:/data
- /mnt/tank/Home/Photos:/externalThen in the web interface, Administration, External Libraries, add /external -
the path inside the container, not the host path.
Two things to understand about external libraries. Immich will not move, rename or delete those files, which is the point. And if a file disappears from the folder, Immich marks the asset offline rather than deleting your metadata, so a temporarily unmounted disk does not destroy your albums.
Immich backs up its own database. Check where.
Immich runs a database dump on a schedule and writes it into the library folder:
ls -la /mnt/tank/Immich/backups/ | tail -5This is genuinely useful and many people do not know it exists.
It also has an obvious flaw if you leave it alone. Those dumps sit on the same pool as the photos they describe. Lose the pool and you lose the photos and the only thing that could rebuild their metadata, in the same event.
Snapshots are not backups. A daily snapshot of the library dataset does capture those dumps, and it protects you from deleting something or from a bad upgrade. It does not protect you from losing the pool, because it lives on the pool. What you want is the snapshots replicated to a second machine:
zfs list -o name | grep -i immichRun that on the backup machine. If the Immich datasets appear there, the photos and the dumps are both off the box.
If you replicate both the library and the live database directory, know the difference between them. A replicated Postgres data directory is crash-consistent as of the snapshot - usually restorable, but a database captured mid-write is not guaranteed clean. The nightly SQL dump is a proper consistent export.
In a real recovery, reach for the dump first and treat the replicated data directory as the fast option to try second. Having both is the right answer; assuming they are equivalent is not.
After it is running
Check these in order:
docker exec immich_machine_learning ls -l /dev/dri- device present- The logs show
OpenVINOExecutionProviderfirst - Trigger a smart search job and watch the iGPU clock climb:
cat /sys/class/drm/card0/gt_act_freq_mhz - Search for a person by name. If they are there, your migration kept the database, and you have kept everything that mattered.