diff --git a/modules/nixos/bundles/common/default.nix b/modules/nixos/bundles/common/default.nix index 21ed664..fd3f2d6 100644 --- a/modules/nixos/bundles/common/default.nix +++ b/modules/nixos/bundles/common/default.nix @@ -88,6 +88,7 @@ in { "forgejo" "grafana" "asciinema" + "immich" "ntfy" "terraform" "vaultwarden" diff --git a/modules/nixos/bundles/common/networking.nix b/modules/nixos/bundles/common/networking.nix index f15fd70..8414ce9 100644 --- a/modules/nixos/bundles/common/networking.nix +++ b/modules/nixos/bundles/common/networking.nix @@ -78,6 +78,14 @@ in { port = 5125; internal = true; }; + immich = { + port = 2283; + internal = true; + websocket = { + enable = true; + path = ""; + }; + }; jellyfin = { port = 8096; internal = true; diff --git a/modules/nixos/services/apps/immich/default.nix b/modules/nixos/services/apps/immich/default.nix new file mode 100644 index 0000000..825c2c6 --- /dev/null +++ b/modules/nixos/services/apps/immich/default.nix @@ -0,0 +1,58 @@ +{ + config, + lib, + namespace, + ... +}: let + inherit (lib) mkIf; + inherit (lib.${namespace}) getAttrByNamespace resolveDatabaseHost resolveDatabaseIP hostHasService resolveServicePort; + inherit (config.networking) hostName; + + pgCfg = getAttrByNamespace config "${namespace}.services.storage.postgresql"; + networkCfg = getAttrByNamespace config "${namespace}.services.networking"; + + isEnabled = hostHasService networkCfg.network-services hostName "immich"; + port = resolveServicePort networkCfg.network-services "immich" 2283; +in { + config = mkIf isEnabled { + services.immich = { + enable = true; + host = "0.0.0.0"; + port = port; + openFirewall = true; + + database = { + enable = false; + host = resolveDatabaseIP networkCfg.devices pgCfg.databases "immich"; + port = 5600; + name = "immich"; + user = "immich"; + }; + + secretsFile = config.sops.secrets."environment-file/immich".path; + + settings.server.externalDomain = "https://photos.yumeami.sh"; + }; + + systemd.services.immich-server = let + dbHost = resolveDatabaseHost pgCfg.databases "immich"; + in + mkIf (dbHost == hostName) { + after = ["postgresql.service" "pgbouncer.service"]; + requires = ["postgresql.service" "pgbouncer.service"]; + }; + + sops.secrets."environment-file/immich" = {}; + + ${namespace}.services.storage.impermanence.folders = let + immichUser = config.users.users.immichUser; + in [ + { + directory = "/var/lib/immich"; + user = immichUser.name; + group = immichUser.group; + mode = "700"; + } + ]; + }; +} diff --git a/modules/nixos/services/storage/postgresql/default.nix b/modules/nixos/services/storage/postgresql/default.nix index d501256..b09c096 100644 --- a/modules/nixos/services/storage/postgresql/default.nix +++ b/modules/nixos/services/storage/postgresql/default.nix @@ -6,7 +6,7 @@ pkgs, ... } @ args: let - inherit (lib) types mkIf concatStringsSep hasAttr getAttr filter; + inherit (lib) types mkIf mkMerge concatStringsSep hasAttr getAttr filter optionalString head splitString; inherit (lib.${namespace}) getAttrByNamespace mkOptionsWithNamespace readJsonOrEmpty getIn mkOptAttrset; inherit (config.networking) hostName; base = "${namespace}.services.storage.postgresql"; @@ -26,7 +26,7 @@ in { config = mkIf (hasAttr hostName cfg.databases) (let hostDatabases = getAttr hostName cfg.databases; mainServices = filter (db: !(lib.hasSuffix "-log" db)) hostDatabases; - logDatabases = filter (lib.hasSuffix "-log") hostDatabases; + auxDbs = filter (lib.hasSuffix "-log") hostDatabases; in { services = { postgresql = { @@ -34,9 +34,21 @@ in { enableTCPIP = true; package = pkgs.postgresql_17; - settings = { - port = port; - }; + settings = mkMerge [ + { + port = port; + } + (mkIf (builtins.elem "immich" mainServices) { + shared_preload_libraries = ["vchord.so"]; + search_path = "\"$user\", public, vectors"; + }) + ]; + + extensions = mkIf (builtins.elem "immich" mainServices) (ps: + with ps; [ + pgvector + vectorchord + ]); authentication = let permissionEntries = @@ -105,14 +117,28 @@ in { ALTER FUNCTION public.pgbouncer_lookup(text) SET search_path = pg_catalog; GRANT EXECUTE ON FUNCTION public.pgbouncer_lookup(text) TO pgbouncer_auth; - ${concatStringsSep "\n" (map (logDb: let - mainUser = lib.removeSuffix "-log" logDb; + ${ + auxDbs + |> map (db: let + user = head (splitString "-" db); in '' - GRANT ALL PRIVILEGES ON DATABASE "${logDb}" TO "${mainUser}"; - ALTER DATABASE "${logDb}" OWNER TO "${mainUser}"; + GRANT ALL PRIVILEGES ON DATABASE "${db}" TO "${user}"; + ALTER DATABASE "${db}" OWNER TO "${user}"; '') - logDatabases)} + |> concatStringsSep "\n" + } SQL + + ${ + optionalString (builtins.elem "immich" mainServices) '' + ${ + ["unaccent" "uuid-ossp" "cube" "earthdistance" "pg_trgm" "vector" "vchord"] + |> map (ext: "psql -d immich -v ON_ERROR_STOP=1 -c 'CREATE EXTENSION IF NOT EXISTS \"${ext}\";'") + |> concatStringsSep "\n" + } + psql -d immich -v ON_ERROR_STOP=1 -c 'ALTER SCHEMA public OWNER TO immich;' + '' + } ''; }); } diff --git a/secrets/crypt/postgresql.json b/secrets/crypt/postgresql.json index 5eab980..1a30e7d 100644 Binary files a/secrets/crypt/postgresql.json and b/secrets/crypt/postgresql.json differ diff --git a/secrets/sops/secrets.yaml b/secrets/sops/secrets.yaml index e2ba35b..8de3634 100755 --- a/secrets/sops/secrets.yaml +++ b/secrets/sops/secrets.yaml @@ -16,6 +16,7 @@ environment-file: autobrr: ENC[AES256_GCM,data:EXuYnWKHk0cwFWdMUaU/aapaPlY4o0/WjwlBenIM9aqOQ9jZ8bqQEcSeP8iHg9XjDJFNpxwSTsDCp3sB6jXrqAbR6Q==,iv:rhRvRE5+1VfcGcWjm4rGrs8fPzttp5WgHGpjnwcgSV4=,tag:RNK3Xp+NGeUSOgWC9uUsCA==,type:str] bazarr: ENC[AES256_GCM,data:vDTjMrKEoGdnx1ABTAuA0k3kogR2tV4z9jvXVj52B2YVDRa4MPN/CWRXtAR1u0jysf7rE3NJMsBv3nQgOpI=,iv:ahQlJt+aok0vkgVD85qdpIEX0rxE9qcUOQWH7abqa0w=,tag:DzogkL9XBDJNBsiUgBwHIw==,type:str] glance: ENC[AES256_GCM,data:/T/ItEwjzvLj1/p3ldFu+dorzOWUSDcxrljPiYJ1j/9PIJZMJkZ2BYIXIt8FwwnA1Omwl6QoJwN9H/GiT2we69ucVnic0i3SchDHuM3rXA9/xuvUB61rzXdx3fQ9T7LbJ6cA77oApCBCOA==,iv:LlsIVEhUot5YJpTzi6uvL/i2hrfAxnP3pZOOV9L8Xvs=,tag:/Fnh+WgtZXfSb9pWe1m1Mw==,type:str] + immich: ENC[AES256_GCM,data:ebaICuhFvHeJek8px2PLPYHY21rrfDe3QMj/WSwsnfCrzKnZ20eKxyU2InzSi0KN2hJ4xuhYeJI=,iv:wFMblYyr+ENXQxZwBhQLBvDA4ERmZPjkurn/pIuzYc4=,tag:0OZg/lGC/Lti2XtfAXZXUg==,type:str] lidarr: ENC[AES256_GCM,data:F75i9YOqcFPVICggm+JHJB+jor2PNAAfyu/Zsj+zptqtJh0nJMMpfctuEYRyc1k0y/VGssPDu8FqEqH44kmgn4L1cmsLcAM=,iv:6JvmnIAG4GUebD7DoLrGt/G/tUrbAiZUDlGZUHxxmbY=,tag:U9wB+cKXM3kSnbADmfTmzw==,type:str] ntfy: ENC[AES256_GCM,data:JB+h4r+QXerD8PbT35RwM556RY63mTNvBN31rIZA7gVkqfa33tLco1fNkFyysA+qpTKaraKpGgmlC6yBgWi5dc7My2CzEHKqqEAV6CJ24Tj00F1aXOunw8BsEBCYuVeHpbIsEtikbkTQ6d0r5bAXdlrL8zyD,iv:/zLYKwDcaNWR3lBqH+IPuwsPnq59SQHaMGSEY+ADJ/w=,tag:+WMzRX+YPaW8dMvJsAAcVQ==,type:str] prowlarr: ENC[AES256_GCM,data:l1aDFg/alG7CDkkwufzMjQ2/6ZLnMvBO/GodS2txktvjRCeoc+qfL4Xf4T6JCltVHTHTl8P3Y0GlWoZ0VPbuP/L+UmV/jZKBvA==,iv:xnXc5TGd1zpDCvaN2IzdI2R/t8C0hTJaLVsP6u3fNsU=,tag:4ZkRyc1aCZoY59X9zIV5gw==,type:str] @@ -168,7 +169,7 @@ sops: yUtR1lCjpW5Hpi2vDoom4HDsodeXfazqS+RjpHuKOLeO39bb4KbAhg== -----END AGE ENCRYPTED FILE----- recipient: age1988jz0qfj6m9xfenhd292tkxwyxvzhv79gkgrhm0gep353kej3yqe6zp86 - lastmodified: "2026-06-30T19:23:37Z" - mac: ENC[AES256_GCM,data:FewAMhmopqxq6N6elvjIVmyR+LaQCh/ItQFVU0tSUInp6+qz6jPb1gKYqOI0pOU0C5euBV02K39afY8vrJBc8hM7AD+talKGXmgNULX3f4JWOnNrfOgFhgoqXTgWrU+kAEHmM5ZNlFICYeEow6i+/t6DAlVFuylrCpFokJLH0bU=,iv:7QnAu29thDRu/AamlvyjgZp4MwoiScELDByLSDWTjV0=,tag:y++WaY41XP/ifUo7IqKZZw==,type:str] + lastmodified: "2026-07-01T03:01:47Z" + mac: ENC[AES256_GCM,data:YU7Bz3jfzZBcRS8988PEelyk0ILGDIMYMOm3dmX1CS2vEmPdYS4F90JS5msyniV8FEvygsZg9Es5UvijEvXCwLHFi8DCsisRREQkjng6Y0HlpYxZ3vkX1ayYqNz6HgtZyLh8SeMvxUJO9fI69NzhW9lw7xvgeSnnNz2DVu2kqqg=,iv:tys6hfzw6qUXXMNMtPx8cydGLJLbT6OEBxQaCAt8LwU=,tag:niCSLPegApsGkK02Xh4XSA==,type:str] unencrypted_suffix: _unencrypted version: 3.13.1