Deploying Immich for self-hosted photos: NAS for the library, SSD for the hot path
I wanted out of paying a monthly bill to keep my own photos, and I wanted them on hardware I own. Immich is the self-hosted photo app that actually feels like the cloud service it replaces: a timeline, search, face grouping, and a real mobile app that backs up new photos in the background. The hard part of self-hosting a photo library is not the app, though. It is the storage.
Why Immich
The mobile app is what makes Immich a real replacement rather than a gallery viewer. It backs up your camera roll automatically, and the machine-learning features (search by content, group by face) run against your own library on your own hardware. That combination, auto-backup plus on-device-style search without a cloud, is the thing I did not want to give up by leaving a hosted service.
The storage decision
A photo library is big and mostly cold: thousands of originals that rarely change and only grow. The database, thumbnails, and transcoded video are the opposite, small and hot, touched on every timeline scroll. Putting all of it in one place is the mistake. So I split it.
Originals go on the NAS over SMB. My QNAP NAS has the capacity and its own disk redundancy, so the library can grow without filling the container’s disk. The database, thumbnails, encoded video, and the cache go on the container’s local SSD. Immich’s own documentation is firm that its PostgreSQL database must live on local storage rather than a network share, because the vector search and write latency over SMB will wreck it. Thumbnails and transcodes do not strictly have to be local, but every timeline scroll reads them, so keeping them on SSD is what keeps the UI quick. The env file is really just a list of locations:
UPLOAD_LOCATION=/media/photos # NAS over SMB
THUMB_LOCATION=/opt/immich/data/thumbs # local SSD
ENCODED_VIDEO_LOCATION=/opt/immich/data/encoded-video # local SSD
DB_DATA_LOCATION=/opt/immich/data/postgres # local SSD
The stack, on Docker-in-LXC
Same pattern as the rest of the homelab: a Docker Compose stack inside a Proxmox LXC container, driven by Ansible. Immich is four containers:
immich-server, the API and web app (the background microservices are folded into it now).immich-machine-learning, which does face recognition and smart search. This is the memory-hungry one.- The database, Immich’s own PostgreSQL image with the pgvector extension for similarity search.
- Valkey, the Redis fork, for the job queue.
I gave the container 4 CPUs and 6GB of RAM, and that number is why the project waited. I deferred the deploy until a round of RAM upgrades landed on the cluster, because Immich with machine learning enabled is not shy about memory. Standing it up on a 2GB container would have been miserable.
One small thing worth setting explicitly is the timezone. The compose file mounts /etc/localtime into the container, but I also set TZ in the env, because a container does not reliably inherit the host’s local time from that mount alone. Set it to your zone and your photo timestamps and job logs line up:
TZ=<your-timezone>
Keeping the NAS mount honest
Putting the library on an SMB share introduces a failure mode local disk does not have: the mount can go stale. If the NAS reboots or the network blips, a CIFS mount can stay mounted as far as the kernel is concerned while every read returns an error. Immich would then see an empty or unreadable photo directory and act on it, which is exactly the kind of quiet breakage you do not want under a photo library.
So the mount needs more than an fstab line. The Ansible role installs a small systemd timer that runs on boot and once an hour, and the check it runs is stricter than “is it mounted”:
if ! mountpoint -q "$MOUNT_POINT"; then return 1; fi
# a stale CIFS mount looks mounted but is not actually readable:
if ! timeout 5 stat "$TEST_PATH" >/dev/null 2>&1; then return 1; fi
If the share is not genuinely readable, the script pings the NAS, lazily unmounts the stale handle, and remounts with retries. The other rough edge was the first mount: Ansible’s mount module tripped over permissions on the CIFS share, so the initial mount went in by hand and the role kept the fstab entry for persistence. Not elegant, but the systemd accessibility check is what makes it reliable day to day.
Mobile, and where it landed
The mobile app is the payoff. Point it at the server URL behind Traefik over HTTPS, turn on background backup, and new photos upload on their own. The app handles the timeline, search, and face grouping against the same library on the NAS.
It sits behind Traefik like every other service, replicated to a second node and backed up to Proxmox Backup Server. The originals also live on the NAS with its own redundancy, so the photos are covered twice over.
A few takeaways:
- Split the storage. Originals on bulk network storage, database and thumbnails on local SSD. Immich’s docs require the database to be local; putting thumbnails and transcodes there too is what keeps the UI fast.
- An fstab entry is not enough for a network-mounted library; it needs a liveness check. A stale CIFS mount looks mounted and is not readable, so test accessibility with a timeout and remount if it fails.
- Immich with machine learning wants real RAM. Size the container for the ML service, which dwarfs the web app. I waited for a memory upgrade before deploying, and that was the right call.
- Set
TZexplicitly even when you mount/etc/localtime. The mount alone is not enough inside a container.
Related reading
Researching n8n for the homelab, shelving it, then deploying it anyway
I researched n8n for homelab automation, recommended it, shelved it the same afternoon because I could not justify the maintenance, then came back weeks later and deployed it for a reason I had not planned on.
Deploying paperless-ngx, self-hosted document management with OCR
I wanted to scan a document with my phone, have it OCR'd and searchable, and never touch the paper again. Here is the Docker-in-LXC deploy of paperless-ngx, and the PostgreSQL permission bug that quietly blocked the whole stack.
Researching update automation for the homelab
Twenty-three self-hosted services and no update process beyond "when I remember". I compared Watchtower, Diun, Renovate, and WUD, looked at unattended-upgrades for system packages, and landed on a hybrid plan.
Ready to Transform Your Career?
Let's work together to unlock your potential and achieve your professional goals.