Part 11 of the series: Building a Self-Hosted AI Development Platform
5 min read

Building a Self-Hosted AI Development Platform — Part 11: Music and Photos with Navidrome and Immich

Part 11 of the Forge series: the storage tiers earn their keep — Navidrome as the low-risk first media service, Immich for photos only after the backup posture deserved them, and a 44GB import done safely

Part 11: Music and Photos with Navidrome and Immich

Part 3 built a 5.3TB ZFS mirror and promised it media, photos, and documents. This post keeps the promise. Navidrome now streams the music library, and Immich manages the photo library. Both services follow the storage rules from Part 3 exactly, and the order of their arrival was a deliberate risk decision.

Music First, Photos Later, On Purpose

The build notes contain a rule for storage-heavy services: install one at a time, and never several in one change window. The candidate list was Navidrome for music, Jellyfin for movies, and Immich for photos. Navidrome went first, and the reason is worth stating plainly.

Music files are replaceable. The library is ripped CDs and purchased downloads, and every file has another copy somewhere. Photos are not replaceable. A photo service can become the primary home of a family archive very quickly, and a storage mistake there is permanent. So the low-stakes service tested the storage layout, and the high-stakes service waited until the backup posture was proven.

That patience had a measurable price: months without a photo service. It also had a measurable payoff. When Immich arrived, the HDD mount, the permission mapping, and the backup jobs were all boring, tested infrastructure.

Navidrome is a small music server that speaks the Subsonic API. Its deployment follows the two-tier rule from Part 3:

Path In the container Purpose
/mnt/forge-hdd/appdata/navidrome /data Database, cache, and application state
/mnt/forge-hdd/media/music /music (read-only) The music library

The read-only mount on the library is a deliberate boundary. A streaming service reads music. It does not write music. Files enter the library through a separate transfer path such as rsync or SFTP. If Navidrome has a defect, or if I make a configuration mistake, the library cannot change. The blast radius of the whole service is its own application state.

One access quirk taught a useful lesson. The public hostname sits behind Cloudflare Access, like every browser-facing service in Part 8. Subsonic client applications cannot complete a browser login flow. They connect on the LAN instead, and they still authenticate with their Navidrome account. The lesson recurs in this series: identity checks that assume a browser exclude every client that is not one.

Immich: Higher Stakes, Stricter Rules

Immich is a self-hosted photo service with a timeline, search, machine-learning tagging, and mobile apps. The build note for it opens with the important sentence: backups must be part of the service, not a later addition.

Its storage spans both tiers, and each path has a reason:

Path Purpose
/mnt/forge-hdd/photos/immich/library Uploaded photos and videos, on the mirror
/mnt/forge-hdd/photos/imports Read-only staging for external imports
/opt/platform/data/immich/postgres The Immich database, on SSD
immich_model-cache volume Machine-learning model cache

The split repeats the pattern from Navidrome at a larger scale. Photos are large, cold, and irreplaceable, so they live on the ZFS mirror. The database is small, hot, and rebuilt from dumps, so it stays on the SSD tier. A database inside the media tree puts the highest-churn files on the slowest disks, and it tangles two backup strategies into one directory.

The 44GB Import, Done the Careful Way

The first real test was an Apple Photos archive: 18,905 files, 44.78GB. The tempting method is a direct copy into the library directory that Immich manages. The build notes forbid it, with reason. Immich owns that tree. It renames, transcodes, and indexes what it finds there. Foreign files dropped into a managed tree create a state that neither the human nor the software fully owns.

The safe pattern uses an external library instead:

  1. Copy the export to the staging path /mnt/forge-hdd/photos/imports/apple-photos/.
  2. Use rsync with excludes for .DS_Store and AppleDouble ._* files.
  3. Mount the imports tree into the container read-only, at /external.
  4. Create an Immich external library that points at the import path.
  5. Run a scan, and let Immich index the files where they are.

The read-only mount does the same work as Navidrome’s. Immich can index the archive. It cannot change it. After the scan, the checklist is short: spot-check dates in the timeline, make sure that HEIC files, Live Photos, and videos appear, and examine the job queue for failures.

The Production-Safe Line

The Immich build note draws one line in bold, and it belongs in this post too. Immich is not production-safe for irreplaceable photos until the library, the import staging, and the PostgreSQL data are all inside a tested backup with a proven restore. A backup that has never restored is a hope, not a backup. Part 12 shows the same discipline applied to the identity service, where a full restore proof ran before the service earned trust.

The same caution applies to upgrades. Immich moves fast, and its official Compose file changes between releases. The tracked Compose file gets a manual comparison against the current official one before each upgrade. A blind upgrade of a photo service is a bet with family photos as the stake.

One Loose End: Signing In

Immich has mobile apps, and mobile apps meet the same wall as the Subsonic clients: a native app cannot complete a Cloudflare Access browser flow. For music, the LAN workaround is acceptable. For photos, sign-in from anywhere is the point of the service.

The answer changed the platform’s identity model. The photo hostname bypasses Cloudflare Access, and Immich authenticates against a self-hosted identity provider through OpenID Connect instead. That provider, why it exists, and the recovery proof that made it trustworthy are the next post.

What This Stage Delivered

The mirror now holds a streamed music library and an indexed photo archive, each behind its own read-only boundary. The tier rules from Part 3 survived contact with two real services without an exception. And the platform crossed a quiet line: it now holds data that matters to people who are not me. That is the strongest argument yet for the backup discipline the early parts insisted on.

Next in the Series

Part 12 covers identity. Authentik becomes the platform’s single browser login, with the OIDC integration that lets the Immich iPhone app sign in. It also covers the decision to run the identity worker without a Docker socket, and the disposable-stack restore that proved the recovery set before the service became a dependency.