···44# Architecture:
55# Immich server (127.0.0.1:cfg.immich.port)
66# ↑ reverse proxy
77-# Caddy (http://immich.ewancroft.uk:cfg.immich.caddyPort)
88-# ↑ Cloudflare Tunnel (outbound only, no firewall ports needed)
77+# Caddy (https://immich.ewancroft.uk — tailnet only)
98#
1010-# Storage — shared with Nextcloud:
1111-# Media lives at cfg.immich.mediaDir, which defaults to
1212-# /srv/nextcloud/data/ewan/files/Photos — inside the Nextcloud user files
1313-# tree. This means:
1414-# • Photos uploaded via the Immich mobile app appear in Nextcloud.
1515-# • Photos synced to Nextcloud via the desktop client appear in Immich
1616-# (picked up by the nextcloud-files-scan timer, which runs daily).
1717-#
1818-# Permissions:
1919-# The `immich` user is added to the `nextcloud` group. The Photos directory
2020-# is created with mode 2770 (setgid) so all files created inside inherit
2121-# the nextcloud group, keeping Nextcloud happy when it scans them.
2222-# Nextcloud's own data directory is group-traversable (0750) by default,
2323-# so group membership is sufficient for access.
99+# Storage:
1010+# Media lives at cfg.immich.mediaDir (default: /srv/immich), owned by the
1111+# immich user. Completely independent of Nextcloud.
2412#
2513# Database & cache:
2614# PostgreSQL and Redis are managed locally by NixOS (separate instances
···2816#
2917# First-run:
3018# Navigate to https://immich.ewancroft.uk and complete the onboarding
3131-# wizard to create your admin account. Then configure the library path
3232-# to cfg.immich.mediaDir from the UI.
1919+# wizard to create your admin account.
3320##############################################################################
3421{
3522 config,
···4229in
4330lib.mkIf cfg.services.immich.enable {
44314545- # ── User / group ─────────────────────────────────────────────────────────
4646- # Add immich to the nextcloud group so it can read/write inside the
4747- # Nextcloud data tree with the setgid directories created below.
4848- users.users.immich = {
4949- extraGroups = [ "nextcloud" ];
5050- };
5151-5232 # ── Storage ───────────────────────────────────────────────────────────────
5353- # Create the Photos directory inside the Nextcloud user files tree.
5454- # Mode 2770: owner=nextcloud, group=nextcloud, setgid so new files
5555- # inherit the group — Nextcloud's scanner expects group-owned files.
5633 systemd.tmpfiles.rules = [
5757- # Ensure the Media parent (shared with Jellyfin) exists even if jellyfin.nix
5858- # is disabled. Referencing cfg.jellyfin.mediaDir directly avoids builtins.dirOf,
5959- # which strips option context and causes an activation-script derivation warning.
6060- "d ${cfg.jellyfin.mediaDir} 2770 nextcloud nextcloud -"
6161- "d ${im.mediaDir} 2770 nextcloud nextcloud -"
3434+ "d ${im.mediaDir} 0770 immich immich -"
6235 ];
63366437 # ── Immich service ────────────────────────────────────────────────────────
···6942 host = "127.0.0.1";
7043 port = im.port;
71447272- # Point at the shared directory inside the Nextcloud data tree.
7345 mediaLocation = im.mediaDir;
74467547 # Local PostgreSQL — NixOS creates a dedicated DB and user automatically.
···7951 redis.enable = true;
8052 };
81538282- # Wait for /srv (and Nextcloud's initial setup) before starting Immich,
8383- # so the Photos directory is guaranteed to exist with correct ownership.
5454+ # Wait for /srv before starting Immich so the media directory exists.
8455 systemd.services.immich-server = {
8585- after = [
8686- "srv.mount"
8787- "nextcloud-setup.service"
8888- ];
5656+ after = [ "srv.mount" ];
8957 wants = [ "srv.mount" ];
9058 };
91599260 # ── Caddy reverse proxy ───────────────────────────────────────────────────
9361 # Tailscale direct route — bypasses Cloudflare (no upload size limit).
9462 # Let's Encrypt wildcard cert (*.ewancroft.uk) via Cloudflare DNS-01.
9595- # Reachable from any tailnet device at https://${im.hostname} once split-dns
9696- # is configured in the Tailscale admin console (see modules/split-dns.nix).
9763 services.caddy.virtualHosts."http://${im.hostname}" = lib.mkIf (cfg.server.tailscaleIP != "") {
9864 extraConfig = ''
9965 bind ${cfg.server.tailscaleIP}
+2-6
modules/options.nix
···486486 };
487487 mediaDir = mkOption {
488488 type = str;
489489- default = "/srv/nextcloud/data/ewan/files/Media/Photos";
490490- description = ''
491491- Primary media directory for Immich. Defaults to inside the Nextcloud
492492- user files tree so that photos synced via Nextcloud clients are visible in
493493- Immich, and vice-versa (the nextcloud-files-scan timer picks up writes daily).
494494- Override this if nextcloud.dataDir or nextcloud.adminUser differ from defaults.'';
489489+ default = "/srv/immich";
490490+ description = "Primary media directory for Immich uploads and assets.";
495491 };
496492 };
497493