vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / web-apps / immich.nix
blobc1a30c6ff2b3ce7a8065ae2afcb172b16ece6dba
2   config,
3   lib,
4   pkgs,
5   ...
6 }:
7 let
8   cfg = config.services.immich;
9   isPostgresUnixSocket = lib.hasPrefix "/" cfg.database.host;
10   isRedisUnixSocket = lib.hasPrefix "/" cfg.redis.host;
12   commonServiceConfig = {
13     Type = "simple";
14     Restart = "on-failure";
15     RestartSec = 3;
17     # Hardening
18     CapabilityBoundingSet = "";
19     NoNewPrivileges = true;
20     PrivateUsers = true;
21     PrivateTmp = true;
22     PrivateDevices = true;
23     PrivateMounts = true;
24     ProtectClock = true;
25     ProtectControlGroups = true;
26     ProtectHome = true;
27     ProtectHostname = true;
28     ProtectKernelLogs = true;
29     ProtectKernelModules = true;
30     ProtectKernelTunables = true;
31     RestrictAddressFamilies = [
32       "AF_INET"
33       "AF_INET6"
34       "AF_UNIX"
35     ];
36     RestrictNamespaces = true;
37     RestrictRealtime = true;
38     RestrictSUIDSGID = true;
39   };
40   inherit (lib)
41     types
42     mkIf
43     mkOption
44     mkEnableOption
45     ;
48   options.services.immich = {
49     enable = mkEnableOption "Immich";
50     package = lib.mkPackageOption pkgs "immich" { };
52     mediaLocation = mkOption {
53       type = types.path;
54       default = "/var/lib/immich";
55       description = "Directory used to store media files. If it is not the default, the directory has to be created manually such that the immich user is able to read and write to it.";
56     };
57     environment = mkOption {
58       type = types.submodule { freeformType = types.attrsOf types.str; };
59       default = { };
60       example = {
61         IMMICH_LOG_LEVEL = "verbose";
62       };
63       description = ''
64         Extra configuration environment variables. Refer to the [documentation](https://immich.app/docs/install/environment-variables) for options tagged with 'server', 'api' or 'microservices'.
65       '';
66     };
67     secretsFile = mkOption {
68       type = types.nullOr (
69         types.str
70         // {
71           # We don't want users to be able to pass a path literal here but
72           # it should look like a path.
73           check = it: lib.isString it && lib.types.path.check it;
74         }
75       );
76       default = null;
77       example = "/run/secrets/immich";
78       description = ''
79         Path of a file with extra environment variables to be loaded from disk. This file is not added to the nix store, so it can be used to pass secrets to immich. Refer to the [documentation](https://immich.app/docs/install/environment-variables) for options.
81         To set a database password set this to a file containing:
82         ```
83         DB_PASSWORD=<pass>
84         ```
85       '';
86     };
87     host = mkOption {
88       type = types.str;
89       default = "localhost";
90       description = "The host that immich will listen on.";
91     };
92     port = mkOption {
93       type = types.port;
94       default = 3001;
95       description = "The port that immich will listen on.";
96     };
97     openFirewall = mkOption {
98       type = types.bool;
99       default = false;
100       description = "Whether to open the immich port in the firewall";
101     };
102     user = mkOption {
103       type = types.str;
104       default = "immich";
105       description = "The user immich should run as.";
106     };
107     group = mkOption {
108       type = types.str;
109       default = "immich";
110       description = "The group immich should run as.";
111     };
113     machine-learning = {
114       enable =
115         mkEnableOption "immich's machine-learning functionality to detect faces and search for objects"
116         // {
117           default = true;
118         };
119       environment = mkOption {
120         type = types.submodule { freeformType = types.attrsOf types.str; };
121         default = { };
122         example = {
123           MACHINE_LEARNING_MODEL_TTL = "600";
124         };
125         description = ''
126           Extra configuration environment variables. Refer to the [documentation](https://immich.app/docs/install/environment-variables) for options tagged with 'machine-learning'.
127         '';
128       };
129     };
131     database = {
132       enable =
133         mkEnableOption "the postgresql database for use with immich. See {option}`services.postgresql`"
134         // {
135           default = true;
136         };
137       createDB = mkEnableOption "the automatic creation of the database for immich." // {
138         default = true;
139       };
140       name = mkOption {
141         type = types.str;
142         default = "immich";
143         description = "The name of the immich database.";
144       };
145       host = mkOption {
146         type = types.str;
147         default = "/run/postgresql";
148         example = "127.0.0.1";
149         description = "Hostname or address of the postgresql server. If an absolute path is given here, it will be interpreted as a unix socket path.";
150       };
151       port = mkOption {
152         type = types.port;
153         default = 5432;
154         description = "Port of the postgresql server.";
155       };
156       user = mkOption {
157         type = types.str;
158         default = "immich";
159         description = "The database user for immich.";
160       };
161     };
162     redis = {
163       enable = mkEnableOption "a redis cache for use with immich" // {
164         default = true;
165       };
166       host = mkOption {
167         type = types.str;
168         default = config.services.redis.servers.immich.unixSocket;
169         defaultText = lib.literalExpression "config.services.redis.servers.immich.unixSocket";
170         description = "The host that redis will listen on.";
171       };
172       port = mkOption {
173         type = types.port;
174         default = 0;
175         description = "The port that redis will listen on. Set to zero to disable TCP.";
176       };
177     };
178   };
180   config = mkIf cfg.enable {
181     assertions = [
182       {
183         assertion = !isPostgresUnixSocket -> cfg.secretsFile != null;
184         message = "A secrets file containing at least the database password must be provided when unix sockets are not used.";
185       }
186     ];
188     services.postgresql = mkIf cfg.database.enable {
189       enable = true;
190       ensureDatabases = mkIf cfg.database.createDB [ cfg.database.name ];
191       ensureUsers = mkIf cfg.database.createDB [
192         {
193           name = cfg.database.user;
194           ensureDBOwnership = true;
195           ensureClauses.login = true;
196         }
197       ];
198       extraPlugins = ps: with ps; [ pgvecto-rs ];
199       settings = {
200         shared_preload_libraries = [ "vectors.so" ];
201         search_path = "\"$user\", public, vectors";
202       };
203     };
204     systemd.services.postgresql.serviceConfig.ExecStartPost =
205       let
206         sqlFile = pkgs.writeText "immich-pgvectors-setup.sql" ''
207           CREATE EXTENSION IF NOT EXISTS unaccent;
208           CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
209           CREATE EXTENSION IF NOT EXISTS vectors;
210           CREATE EXTENSION IF NOT EXISTS cube;
211           CREATE EXTENSION IF NOT EXISTS earthdistance;
212           CREATE EXTENSION IF NOT EXISTS pg_trgm;
214           ALTER SCHEMA public OWNER TO ${cfg.database.user};
215           ALTER SCHEMA vectors OWNER TO ${cfg.database.user};
216           GRANT SELECT ON TABLE pg_vector_index_stat TO ${cfg.database.user};
218           ALTER EXTENSION vectors UPDATE;
219         '';
220       in
221       [
222         ''
223           ${lib.getExe' config.services.postgresql.package "psql"} -d "${cfg.database.name}" -f "${sqlFile}"
224         ''
225       ];
227     services.redis.servers = mkIf cfg.redis.enable {
228       immich = {
229         enable = true;
230         user = cfg.user;
231         port = cfg.redis.port;
232         bind = mkIf (!isRedisUnixSocket) cfg.redis.host;
233       };
234     };
236     networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
238     services.immich.environment =
239       let
240         postgresEnv =
241           if isPostgresUnixSocket then
242             { DB_URL = "socket://${cfg.database.host}?dbname=${cfg.database.name}"; }
243           else
244             {
245               DB_HOSTNAME = cfg.database.host;
246               DB_PORT = toString cfg.database.port;
247               DB_DATABASE_NAME = cfg.database.name;
248               DB_USERNAME = cfg.database.user;
249             };
250         redisEnv =
251           if isRedisUnixSocket then
252             { REDIS_SOCKET = cfg.redis.host; }
253           else
254             {
255               REDIS_PORT = toString cfg.redis.port;
256               REDIS_HOSTNAME = cfg.redis.host;
257             };
258       in
259       postgresEnv
260       // redisEnv
261       // {
262         HOST = cfg.host;
263         IMMICH_PORT = toString cfg.port;
264         IMMICH_MEDIA_LOCATION = cfg.mediaLocation;
265         IMMICH_MACHINE_LEARNING_URL = "http://localhost:3003";
266       };
268     services.immich.machine-learning.environment = {
269       MACHINE_LEARNING_WORKERS = "1";
270       MACHINE_LEARNING_WORKER_TIMEOUT = "120";
271       MACHINE_LEARNING_CACHE_FOLDER = "/var/cache/immich";
272       IMMICH_HOST = "localhost";
273       IMMICH_PORT = "3003";
274     };
276     systemd.services.immich-server = {
277       description = "Immich backend server (Self-hosted photo and video backup solution)";
278       after = [ "network.target" ];
279       wantedBy = [ "multi-user.target" ];
280       inherit (cfg) environment;
282       serviceConfig = commonServiceConfig // {
283         ExecStart = lib.getExe cfg.package;
284         EnvironmentFile = mkIf (cfg.secretsFile != null) cfg.secretsFile;
285         StateDirectory = "immich";
286         RuntimeDirectory = "immich";
287         User = cfg.user;
288         Group = cfg.group;
289       };
290     };
292     systemd.services.immich-machine-learning = mkIf cfg.machine-learning.enable {
293       description = "immich machine learning";
294       after = [ "network.target" ];
295       wantedBy = [ "multi-user.target" ];
296       inherit (cfg.machine-learning) environment;
297       serviceConfig = commonServiceConfig // {
298         ExecStart = lib.getExe (cfg.package.machine-learning.override { immich = cfg.package; });
299         CacheDirectory = "immich";
300         User = cfg.user;
301         Group = cfg.group;
302       };
303     };
305     users.users = mkIf (cfg.user == "immich") {
306       immich = {
307         name = "immich";
308         group = cfg.group;
309         isSystemUser = true;
310       };
311     };
312     users.groups = mkIf (cfg.group == "immich") { immich = { }; };
314     meta.maintainers = with lib.maintainers; [ jvanbruegge ];
315   };