grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / web-apps / immich.nix
blobca6b6dd6241332bf99cef11b74e286fcd1a7a14e
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       user = mkOption {
152         type = types.str;
153         default = "immich";
154         description = "The database user for immich.";
155       };
156     };
157     redis = {
158       enable = mkEnableOption "a redis cache for use with immich" // {
159         default = true;
160       };
161       host = mkOption {
162         type = types.str;
163         default = config.services.redis.servers.immich.unixSocket;
164         defaultText = lib.literalExpression "config.services.redis.servers.immich.unixSocket";
165         description = "The host that redis will listen on.";
166       };
167       port = mkOption {
168         type = types.port;
169         default = 0;
170         description = "The port that redis will listen on. Set to zero to disable TCP.";
171       };
172     };
173   };
175   config = mkIf cfg.enable {
176     assertions = [
177       {
178         assertion = !isPostgresUnixSocket -> cfg.secretsFile != null;
179         message = "A secrets file containing at least the database password must be provided when unix sockets are not used.";
180       }
181     ];
183     services.postgresql = mkIf cfg.database.enable {
184       enable = true;
185       ensureDatabases = mkIf cfg.database.createDB [ cfg.database.name ];
186       ensureUsers = mkIf cfg.database.createDB [
187         {
188           name = cfg.database.user;
189           ensureDBOwnership = true;
190           ensureClauses.login = true;
191         }
192       ];
193       extraPlugins = ps: with ps; [ pgvecto-rs ];
194       settings = {
195         shared_preload_libraries = [ "vectors.so" ];
196         search_path = "\"$user\", public, vectors";
197       };
198     };
199     systemd.services.postgresql.serviceConfig.ExecStartPost =
200       let
201         sqlFile = pkgs.writeText "immich-pgvectors-setup.sql" ''
202           CREATE EXTENSION IF NOT EXISTS unaccent;
203           CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
204           CREATE EXTENSION IF NOT EXISTS vectors;
205           CREATE EXTENSION IF NOT EXISTS cube;
206           CREATE EXTENSION IF NOT EXISTS earthdistance;
207           CREATE EXTENSION IF NOT EXISTS pg_trgm;
209           ALTER SCHEMA public OWNER TO ${cfg.database.user};
210           ALTER SCHEMA vectors OWNER TO ${cfg.database.user};
211           GRANT SELECT ON TABLE pg_vector_index_stat TO ${cfg.database.user};
213           ALTER EXTENSION vectors UPDATE;
214         '';
215       in
216       [
217         ''
218           ${lib.getExe' config.services.postgresql.package "psql"} -d "${cfg.database.name}" -f "${sqlFile}"
219         ''
220       ];
222     services.redis.servers = mkIf cfg.redis.enable {
223       immich = {
224         enable = true;
225         user = cfg.user;
226         port = cfg.redis.port;
227         bind = mkIf (!isRedisUnixSocket) cfg.redis.host;
228       };
229     };
231     networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
233     services.immich.environment =
234       let
235         postgresEnv =
236           if isPostgresUnixSocket then
237             { DB_URL = "socket://${cfg.database.host}?dbname=${cfg.database.name}"; }
238           else
239             {
240               DB_HOSTNAME = cfg.database.host;
241               DB_PORT = toString cfg.database.port;
242               DB_DATABASE_NAME = cfg.database.name;
243               DB_USERNAME = cfg.database.user;
244             };
245         redisEnv =
246           if isRedisUnixSocket then
247             { REDIS_SOCKET = cfg.redis.host; }
248           else
249             {
250               REDIS_PORT = toString cfg.redis.port;
251               REDIS_HOSTNAME = cfg.redis.host;
252             };
253       in
254       postgresEnv
255       // redisEnv
256       // {
257         HOST = cfg.host;
258         IMMICH_PORT = toString cfg.port;
259         IMMICH_MEDIA_LOCATION = cfg.mediaLocation;
260         IMMICH_MACHINE_LEARNING_URL = "http://localhost:3003";
261       };
263     services.immich.machine-learning.environment = {
264       MACHINE_LEARNING_WORKERS = "1";
265       MACHINE_LEARNING_WORKER_TIMEOUT = "120";
266       MACHINE_LEARNING_CACHE_FOLDER = "/var/cache/immich";
267       IMMICH_HOST = "localhost";
268       IMMICH_PORT = "3003";
269     };
271     systemd.services.immich-server = {
272       description = "Immich backend server (Self-hosted photo and video backup solution)";
273       after = [ "network.target" ];
274       wantedBy = [ "multi-user.target" ];
275       inherit (cfg) environment;
277       serviceConfig = commonServiceConfig // {
278         ExecStart = lib.getExe cfg.package;
279         EnvironmentFile = mkIf (cfg.secretsFile != null) cfg.secretsFile;
280         StateDirectory = "immich";
281         RuntimeDirectory = "immich";
282         User = cfg.user;
283         Group = cfg.group;
284       };
285     };
287     systemd.services.immich-machine-learning = mkIf cfg.machine-learning.enable {
288       description = "immich machine learning";
289       after = [ "network.target" ];
290       wantedBy = [ "multi-user.target" ];
291       inherit (cfg.machine-learning) environment;
292       serviceConfig = commonServiceConfig // {
293         ExecStart = lib.getExe (cfg.package.machine-learning.override { immich = cfg.package; });
294         CacheDirectory = "immich";
295         User = cfg.user;
296         Group = cfg.group;
297       };
298     };
300     users.users = mkIf (cfg.user == "immich") {
301       immich = {
302         name = "immich";
303         group = cfg.group;
304         isSystemUser = true;
305       };
306     };
307     users.groups = mkIf (cfg.group == "immich") { immich = { }; };
309     meta.maintainers = with lib.maintainers; [ jvanbruegge ];
310   };