Blog

Immich on the NAS: Ditching the iCloud Photos Subscription

Self-hosting Immich on a NAS to replace the iCloud Photos subscription

At nine in the morning I opened the compose file on the NAS with a very specific goal: by tonight, my own phone's background upload should be running; over the weekend, the rest of the family installs the app. The iCloud photos subscription keeps creeping up every year, and I still want the originals on my own disks. Immich is the closest I've tried to a self-hosted photo library that a non-geek family member can actually use — official app, a proper timeline, face grouping you can switch off, and an upload experience that doesn't drive people away the way Nextcloud's does. Here's that day, told in real time.

09:00 — Split the volumes before you hit up

The official compose boots fine as soon as you pull it, but for long-term running you want to think through the semantics of three kinds of data first, instead of defaulting everything into one volume.

The upload-originals directory is the largest, so mount it straight onto the NAS data pool, e.g. /data/immich/library. Postgres data can't share the "just rsync it casually" strategy with the originals — it needs a path that can be consistency-backed-up on its own. The machine-learning models and thumbnail caches can be rebuilt: if they're lost, a library scan regenerates them, but a rebuild burns CPU and time, so giving them a separate volume means you can later migrate by moving "only originals + database".

16 GB of RAM is a sensible floor. The first face-recognition pass and thumbnail generation will peg the CPU, so don't share an already-warm little box with your game server. Set UPLOAD_LOCATION, the database password, and the time zone in .env; put HTTPS in front through a reverse proxy (Traefik, 1Panel, or Caddy — any of them). The mobile app is picky about self-signed certs; a proper domain certificate saves most of the debugging time.

On the public side I put Immich behind Cloudflare Tunnel + Access instead of leaving it naked on an open port. On the LAN I resolve an internal domain to the NAS so uploads don't loop through the CDN — when the family is on Wi-Fi pushing a few thousand historical photos, that difference in cost and latency is very noticeable.

11:30 — Containers up, test it yourself before touching the phones

After docker compose up -d, watch the logs: Postgres healthy, immich-server and microservices not restarting. Log into the web admin in a browser, create your own user, and turn off public registration while you're there. Upload one local test image and check that it shows up on the timeline and the thumbnail gets generated. If the thumbnail queue is piling up, check whether the machine is also running recognition jobs at the same time; if needed, keep a "upload by day, machine learning at night" rhythm so the little box doesn't throttle from heat.

Don't rush to install it on family phones yet. I threw in a HEIC, a Live Photo, and a short video, one each, and confirmed nothing is "uploadable but not viewable". Face grouping stays off for now — in a family album, "who keeps appearing together" is sometimes fun and sometimes a privacy burden, so that's a conversation to have with the family before turning it on.

14:00 — My own phone eats the historical library first

Install the official app, point the server address at the HTTPS domain, and enable background upload. The first week is really history backfill: battery and heat will be obvious, so keep it plugged in, on home Wi-Fi, and pick "Wi-Fi only" or even "charge only" uploads depending on how your device behaves. Android and iOS background restrictions differ; Xiaomi and Huawei may need you to manually allow auto-start and exempt the app from battery optimization.

The order matters: uploads confirmed clean first, then talk about turning off iCloud Photos. Reverse it and you get a gap — the first time a family member panics about "not finding last year's trip in my phone", the whole self-hosting project gets voted down.

18:00 — Backup strategy goes in the checklist, not in a good mood

An Immich process that crashes can be reinstalled; a library that's lost is really gone. So that evening I turned the strategy into a few executable items instead of "we'll deal with it later":

  • Data pool snapshots (ZFS / TrueNAS): guards against accidental deletion and software bugs wiping the library
  • restic off-site: guards against fire and whole-pool disasters
  • The database uses the official flow or pg_dump, kept separate from the library directory

I only felt safe after running a restore drill once: empty instance → restore database → mount library → trigger a library scan → open a random photo from a year ago and confirm the original downloads. A backup that was never drilled is just a placebo. Before upgrading Immich, snapshot first, then pull: read the database migrations in the release notes — app force-updates sometimes require a minimum server version, and monthly updates beat chasing nightly.

21:00 — The pitch before installing it for the family

When I installed it for the family that evening, I emphasized exactly three things: the address is already configured for you, plug it in and leave it on home Wi-Fi overnight, and the phone will run a bit hot for the first few days. No compose talk, no face-model talk. Shared albums for remote relatives get time-limited links or read-only users — no global registration just for convenience.

Set expectations too: Immich is primary archival plus everyday browsing, not Lightroom. Browsing, searching, and sharing individual albums is plenty; deep edits still go back to the system photo app. Before canceling the iCloud subscription, wait until all three are true — new photos only land in Immich, the family knows how to view the timeline, and you can restore from backup. By the end of that day, the service was running, my phone was uploading, and the backup path was written down in my notes; the family's phones get that one quiet weekend night.

← Back to Blog