# Auto-generated using compose2nix v0.1.6. { pkgs, nodes, config, ... }: let version = "v1.106.4"; immichDomain = "immich.${config.secrets.secrets.global.domains.web}"; ipImmichMachineLearning = "10.89.0.10"; ipImmichPostgres = "10.89.0.12"; ipImmichRedis = "10.89.0.13"; ipImmichServer = "10.89.0.14"; configFile = pkgs.writeText "immich.config.json" ( builtins.toJSON { ffmpeg = { accel = "disabled"; bframes = -1; cqMode = "auto"; crf = 23; gopSize = 0; maxBitrate = "0"; npl = 0; preset = "ultrafast"; refs = 0; targetAudioCodec = "aac"; targetResolution = "720"; targetVideoCodec = "h264"; temporalAQ = false; threads = 0; tonemap = "hable"; transcode = "required"; twoPass = false; }; job = { backgroundTask.concurrency = 5; faceDetection.concurrency = 10; library.concurrency = 5; metadataExtraction.concurrency = 10; migration.concurrency = 5; search.concurrency = 5; sidecar.concurrency = 5; smartSearch.concurrency = 10; thumbnailGeneration.concurrency = 10; videoConversion.concurrency = 5; }; library.scan = { enabled = true; cronExpression = "0 0 * * *"; }; logging = { enabled = true; level = "log"; }; machineLearning = { clip = { enabled = true; modelName = "ViT-B-32__openai"; }; enabled = true; facialRecognition = { enabled = true; maxDistance = 0.45; minFaces = 2; minScore = 0.65; modelName = "buffalo_l"; }; url = "http://${ipImmichMachineLearning}:3003"; }; # XXX: Immich's oauth cannot use PKCE and uses legacy crypto so we need to enable legacy crypto oauth = rec { enabled = true; autoLaunch = false; autoRegister = true; buttonText = "Login with Kanidm"; mobileOverrideEnabled = true; mobileRedirectUri = "https://${immichDomain}/api/oauth/mobile-redirect"; clientId = "immich"; # clientSecret will be dynamically added in activation script issuerUrl = "https://auth.${config.secrets.secrets.global.domains.web}/oauth2/openid/${clientId}"; scope = "openid email profile"; storageLabelClaim = "preferred_username"; }; map = { enabled = true; darkStyle = ""; lightStyle = ""; }; newVersionCheck.enabled = true; passwordLogin.enabled = true; reverseGeocoding.enabled = true; server = { externalDomain = "https://${immichDomain}"; loginPageMessage = "Wilkommen in Patricks tollem bilderparadies"; }; storageTemplate = { enabled = true; hashVerificationEnabled = true; template = "{{y}}/{{MM}}/{{filename}}"; }; theme.customCss = ""; thumbnail = { colorspace = "p3"; jpegSize = 1440; quality = 80; webpSize = 250; }; trash = { days = 30; enabled = true; }; } ); environment = { DB_DATABASE_NAME = "immich"; DB_HOSTNAME = ipImmichPostgres; DB_PASSWORD_FILE = config.age.secrets.postgrespasswd.path; DB_USERNAME = "postgres"; IMMICH_VERSION = "${version}"; UPLOAD_LOCATION = upload_folder; IMMICH_SERVER_URL = "http://${ipImmichServer}:3001/"; IMMICH_MACHINE_LEARNING_URL = "http://${ipImmichMachineLearning}:3003"; REDIS_HOSTNAME = ipImmichRedis; IMMICH_CONFIG_FILE = "/immich.config.json"; }; upload_folder = "/panzer/immich"; pgdata_folder = "/persist/immich/pgdata"; model_folder = "/state/immich/modeldata"; serviceConfig = { serviceConfig = { Restart = "always"; }; after = [ "podman-network-immich-default.service" ]; requires = [ "podman-network-immich-default.service" ]; partOf = [ "podman-compose-immich-root.target" ]; wantedBy = [ "podman-compose-immich-root.target" ]; }; processedConfigFile = "/run/agenix/immich.config.json"; in { age.secrets.resticpasswd = { generator.script = "alnum"; }; age.secrets.immichHetznerSsh = { generator.script = "ssh-ed25519"; }; environment.persistence."/state".directories = [ { directory = "/var/lib/containers"; mode = "0755"; } ]; services.restic.backups = { main = { user = "root"; timerConfig = { OnCalendar = "06:00"; Persistent = true; RandomizedDelaySec = "3h"; }; initialize = true; passwordFile = config.age.secrets.resticpasswd.path; hetznerStorageBox = { enable = true; inherit (config.secrets.secrets.global.hetzner) mainUser; inherit (config.secrets.secrets.global.hetzner.users.immich) subUid path; sshAgeSecret = "immichHetznerSsh"; }; backupPrepareCommand = '' ${pkgs.podman}/bin/podman exec -t immich_postgres pg_dumpall -c -U postgres > /run/immich_dump.sql ''; paths = [ "${upload_folder}/library" "${upload_folder}/upload" "${upload_folder}/profile" "/run/immich_dump.sql" ]; pruneOpts = [ "--keep-daily 10" "--keep-weekly 7" "--keep-monthly 12" "--keep-yearly 75" ]; }; }; # Mirror the original oauth2 secret age.secrets.immich-oauth2-client-secret = { inherit (nodes.elisabeth-kanidm.config.age.secrets.oauth2-immich) rekeyFile; mode = "440"; group = "root"; }; system.activationScripts.agenixRooterDerivedSecrets = { # Run after agenix has generated secrets deps = ["agenix"]; text = '' immichClientSecret=$(< ${config.age.secrets.immich-oauth2-client-secret.path}) ${pkgs.jq}/bin/jq --arg immichClientSecret "$immichClientSecret" '.oauth.clientSecret = $immichClientSecret' ${configFile} > ${processedConfigFile} chmod 444 ${processedConfigFile} ''; }; microvm = { mem = 1024 * 8; vcpu = 12; }; wireguard.elisabeth = { client.via = "elisabeth"; firewallRuleForNode.elisabeth.allowedTCPPorts = [3000]; }; networking.nftables.chains.forward.into-immich-container = { after = ["conntrack"]; rules = [ "iifname elisabeth ip saddr ${nodes.elisabeth.config.wireguard.elisabeth.ipv4} tcp dport 3001 accept" "iifname podman1 oifname lan accept" ]; }; systemd.tmpfiles.settings = { "10-immich" = { ${upload_folder}.d = { mode = "0770"; }; ${pgdata_folder}.d = { mode = "0770"; }; ${model_folder}.d = { mode = "0770"; }; }; }; age.secrets.postgrespasswd = { generator.script = "alnum"; }; age.secrets.redispasswd = { generator.script = "alnum"; }; # Runtime virtualisation.podman = { enable = true; autoPrune.enable = true; dockerCompat = true; }; virtualisation.oci-containers.backend = "podman"; # Containers virtualisation.oci-containers.containers."immich_machine_learning" = { image = "ghcr.io/immich-app/immich-machine-learning:${version}"; inherit environment; volumes = [ "${processedConfigFile}:${environment.IMMICH_CONFIG_FILE}:ro" "${model_folder}:/cache:rw" ]; log-driver = "journald"; extraOptions = [ "--network-alias=immich-machine-learning" "--network=immich-default" "--ip=${ipImmichMachineLearning}" ]; }; systemd.services."podman-immich_machine_learning" = serviceConfig; virtualisation.oci-containers.containers."immich_postgres" = { image = "tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0"; environment = { POSTGRES_DB = environment.DB_DATABASE_NAME; POSTGRES_PASSWORD_FILE = environment.DB_PASSWORD_FILE; POSTGRES_USER = environment.DB_USERNAME; }; volumes = [ "${pgdata_folder}:/var/lib/postgresql/data:rw" "${environment.DB_PASSWORD_FILE}:${environment.DB_PASSWORD_FILE}:ro" ]; log-driver = "journald"; extraOptions = [ "--network-alias=immich_postgres" "--network=immich-default" "--ip=${ipImmichPostgres}" ]; }; systemd.services."podman-immich_postgres" = serviceConfig; virtualisation.oci-containers.containers."immich_redis" = { image = "redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5"; log-driver = "journald"; extraOptions = [ "--network-alias=immich_redis" "--network=immich-default" "--ip=${ipImmichRedis}" ]; }; systemd.services."podman-immich_redis" = serviceConfig; virtualisation.oci-containers.containers."immich_server" = { image = "ghcr.io/immich-app/immich-server:${version}"; inherit environment; volumes = [ "${processedConfigFile}:${environment.IMMICH_CONFIG_FILE}:ro" "/etc/localtime:/etc/localtime:ro" "${upload_folder}:/usr/src/app/upload:rw" "${environment.DB_PASSWORD_FILE}:${environment.DB_PASSWORD_FILE}:ro" ]; ports = [ "3000:3001/tcp" ]; dependsOn = [ "immich_postgres" "immich_redis" ]; log-driver = "journald"; extraOptions = [ "--network-alias=immich-server" "--network=immich-default" "--ip=${ipImmichServer}" ]; }; systemd.services."podman-immich_server" = serviceConfig // { unitConfig.UpheldBy = [ "podman-immich_postgres.service" "podman-immich_redis.service" ]; }; # Networks systemd.services."podman-network-immich-default" = { path = [pkgs.podman]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; ExecStop = "${pkgs.podman}/bin/podman network rm -f immich-default"; }; script = '' podman network inspect immich-default || podman network create immich-default --opt isolate=true --disable-dns --subnet=10.89.0.0/24 ''; partOf = ["podman-compose-immich-root.target"]; wantedBy = ["podman-compose-immich-root.target"]; }; # Root service # When started, this will automatically create all resources and start # the containers. When stopped, this will teardown all resources. systemd.targets."podman-compose-immich-root" = { unitConfig = { Description = "Root target generated by compose2nix."; }; wantedBy = ["multi-user.target"]; }; }