grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / databases / redis.nix
blob7a3f408aa98e4332e751020c3235978404014df0
1 { config, lib, pkgs, ... }:
3 with lib;
5 let
6   cfg = config.services.redis;
8   mkValueString = value:
9     if value == true then "yes"
10     else if value == false then "no"
11     else generators.mkValueStringDefault { } value;
13   redisConfig = settings: pkgs.writeText "redis.conf" (generators.toKeyValue {
14     listsAsDuplicateKeys = true;
15     mkKeyValue = generators.mkKeyValueDefault { inherit mkValueString; } " ";
16   } settings);
18   redisName = name: "redis" + optionalString (name != "") ("-"+name);
19   enabledServers = filterAttrs (name: conf: conf.enable) config.services.redis.servers;
21 in {
22   imports = [
23     (mkRemovedOptionModule [ "services" "redis" "user" ] "The redis module now is hardcoded to the redis user.")
24     (mkRemovedOptionModule [ "services" "redis" "dbpath" ] "The redis module now uses /var/lib/redis as data directory.")
25     (mkRemovedOptionModule [ "services" "redis" "dbFilename" ] "The redis module now uses /var/lib/redis/dump.rdb as database dump location.")
26     (mkRemovedOptionModule [ "services" "redis" "appendOnlyFilename" ] "This option was never used.")
27     (mkRemovedOptionModule [ "services" "redis" "pidFile" ] "This option was removed.")
28     (mkRemovedOptionModule [ "services" "redis" "extraConfig" ] "Use services.redis.servers.*.settings instead.")
29     (mkRenamedOptionModule [ "services" "redis" "enable"] [ "services" "redis" "servers" "" "enable" ])
30     (mkRenamedOptionModule [ "services" "redis" "port"] [ "services" "redis" "servers" "" "port" ])
31     (mkRenamedOptionModule [ "services" "redis" "openFirewall"] [ "services" "redis" "servers" "" "openFirewall" ])
32     (mkRenamedOptionModule [ "services" "redis" "bind"] [ "services" "redis" "servers" "" "bind" ])
33     (mkRenamedOptionModule [ "services" "redis" "unixSocket"] [ "services" "redis" "servers" "" "unixSocket" ])
34     (mkRenamedOptionModule [ "services" "redis" "unixSocketPerm"] [ "services" "redis" "servers" "" "unixSocketPerm" ])
35     (mkRenamedOptionModule [ "services" "redis" "logLevel"] [ "services" "redis" "servers" "" "logLevel" ])
36     (mkRenamedOptionModule [ "services" "redis" "logfile"] [ "services" "redis" "servers" "" "logfile" ])
37     (mkRenamedOptionModule [ "services" "redis" "syslog"] [ "services" "redis" "servers" "" "syslog" ])
38     (mkRenamedOptionModule [ "services" "redis" "databases"] [ "services" "redis" "servers" "" "databases" ])
39     (mkRenamedOptionModule [ "services" "redis" "maxclients"] [ "services" "redis" "servers" "" "maxclients" ])
40     (mkRenamedOptionModule [ "services" "redis" "save"] [ "services" "redis" "servers" "" "save" ])
41     (mkRenamedOptionModule [ "services" "redis" "slaveOf"] [ "services" "redis" "servers" "" "slaveOf" ])
42     (mkRenamedOptionModule [ "services" "redis" "masterAuth"] [ "services" "redis" "servers" "" "masterAuth" ])
43     (mkRenamedOptionModule [ "services" "redis" "requirePass"] [ "services" "redis" "servers" "" "requirePass" ])
44     (mkRenamedOptionModule [ "services" "redis" "requirePassFile"] [ "services" "redis" "servers" "" "requirePassFile" ])
45     (mkRenamedOptionModule [ "services" "redis" "appendOnly"] [ "services" "redis" "servers" "" "appendOnly" ])
46     (mkRenamedOptionModule [ "services" "redis" "appendFsync"] [ "services" "redis" "servers" "" "appendFsync" ])
47     (mkRenamedOptionModule [ "services" "redis" "slowLogLogSlowerThan"] [ "services" "redis" "servers" "" "slowLogLogSlowerThan" ])
48     (mkRenamedOptionModule [ "services" "redis" "slowLogMaxLen"] [ "services" "redis" "servers" "" "slowLogMaxLen" ])
49     (mkRenamedOptionModule [ "services" "redis" "settings"] [ "services" "redis" "servers" "" "settings" ])
50   ];
52   ###### interface
54   options = {
56     services.redis = {
57       package = mkPackageOption pkgs "redis" { };
59       vmOverCommit = mkEnableOption ''
60         set `vm.overcommit_memory` sysctl to 1
61         (Suggested for Background Saving: <https://redis.io/docs/get-started/faq/>)
62       '' // { default = true; };
64       servers = mkOption {
65         type = with types; attrsOf (submodule ({ config, name, ... }: {
66           options = {
67             enable = mkEnableOption "Redis server";
69             user = mkOption {
70               type = types.str;
71               default = redisName name;
72               defaultText = literalExpression ''
73                 if name == "" then "redis" else "redis-''${name}"
74               '';
75               description = "The username and groupname for redis-server.";
76             };
78             port = mkOption {
79               type = types.port;
80               default = if name == "" then 6379 else 0;
81               defaultText = literalExpression ''if name == "" then 6379 else 0'';
82               description = ''
83                 The TCP port to accept connections.
84                 If port 0 is specified Redis will not listen on a TCP socket.
85               '';
86             };
88             openFirewall = mkOption {
89               type = types.bool;
90               default = false;
91               description = ''
92                 Whether to open ports in the firewall for the server.
93               '';
94             };
96             extraParams = mkOption {
97               type = with types; listOf str;
98               default = [];
99               description = "Extra parameters to append to redis-server invocation";
100               example = [ "--sentinel" ];
101             };
103             bind = mkOption {
104               type = with types; nullOr str;
105               default = "127.0.0.1";
106               description = ''
107                 The IP interface to bind to.
108                 `null` means "all interfaces".
109               '';
110               example = "192.0.2.1";
111             };
113             unixSocket = mkOption {
114               type = with types; nullOr path;
115               default = "/run/${redisName name}/redis.sock";
116               defaultText = literalExpression ''
117                 if name == "" then "/run/redis/redis.sock" else "/run/redis-''${name}/redis.sock"
118               '';
119               description = "The path to the socket to bind to.";
120             };
122             unixSocketPerm = mkOption {
123               type = types.int;
124               default = 660;
125               description = "Change permissions for the socket";
126               example = 600;
127             };
129             logLevel = mkOption {
130               type = types.str;
131               default = "notice"; # debug, verbose, notice, warning
132               example = "debug";
133               description = "Specify the server verbosity level, options: debug, verbose, notice, warning.";
134             };
136             logfile = mkOption {
137               type = types.str;
138               default = "/dev/null";
139               description = "Specify the log file name. Also 'stdout' can be used to force Redis to log on the standard output.";
140               example = "/var/log/redis.log";
141             };
143             syslog = mkOption {
144               type = types.bool;
145               default = true;
146               description = "Enable logging to the system logger.";
147             };
149             databases = mkOption {
150               type = types.int;
151               default = 16;
152               description = "Set the number of databases.";
153             };
155             maxclients = mkOption {
156               type = types.int;
157               default = 10000;
158               description = "Set the max number of connected clients at the same time.";
159             };
161             save = mkOption {
162               type = with types; listOf (listOf int);
163               default = [ [900 1] [300 10] [60 10000] ];
164               description = ''
165                 The schedule in which data is persisted to disk, represented as a list of lists where the first element represent the amount of seconds and the second the number of changes.
167                 If set to the empty list (`[]`) then RDB persistence will be disabled (useful if you are using AOF or don't want any persistence).
168               '';
169             };
171             slaveOf = mkOption {
172               type = with types; nullOr (submodule ({ ... }: {
173                 options = {
174                   ip = mkOption {
175                     type = str;
176                     description = "IP of the Redis master";
177                     example = "192.168.1.100";
178                   };
180                   port = mkOption {
181                     type = port;
182                     description = "port of the Redis master";
183                     default = 6379;
184                   };
185                 };
186               }));
188               default = null;
189               description = "IP and port to which this redis instance acts as a slave.";
190               example = { ip = "192.168.1.100"; port = 6379; };
191             };
193             masterAuth = mkOption {
194               type = with types; nullOr str;
195               default = null;
196               description = ''If the master is password protected (using the requirePass configuration)
197               it is possible to tell the slave to authenticate before starting the replication synchronization
198               process, otherwise the master will refuse the slave request.
199               (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE)'';
200             };
202             requirePass = mkOption {
203               type = with types; nullOr str;
204               default = null;
205               description = ''
206                 Password for database (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE).
207                 Use requirePassFile to store it outside of the nix store in a dedicated file.
208               '';
209               example = "letmein!";
210             };
212             requirePassFile = mkOption {
213               type = with types; nullOr path;
214               default = null;
215               description = "File with password for the database.";
216               example = "/run/keys/redis-password";
217             };
219             appendOnly = mkOption {
220               type = types.bool;
221               default = false;
222               description = "By default data is only periodically persisted to disk, enable this option to use an append-only file for improved persistence.";
223             };
225             appendFsync = mkOption {
226               type = types.str;
227               default = "everysec"; # no, always, everysec
228               description = "How often to fsync the append-only log, options: no, always, everysec.";
229             };
231             slowLogLogSlowerThan = mkOption {
232               type = types.int;
233               default = 10000;
234               description = "Log queries whose execution take longer than X in milliseconds.";
235               example = 1000;
236             };
238             slowLogMaxLen = mkOption {
239               type = types.int;
240               default = 128;
241               description = "Maximum number of items to keep in slow log.";
242             };
244             settings = mkOption {
245               # TODO: this should be converted to freeformType
246               type = with types; attrsOf (oneOf [ bool int str (listOf str) ]);
247               default = {};
248               description = ''
249                 Redis configuration. Refer to
250                 <https://redis.io/topics/config>
251                 for details on supported values.
252               '';
253               example = literalExpression ''
254                 {
255                   loadmodule = [ "/path/to/my_module.so" "/path/to/other_module.so" ];
256                 }
257               '';
258             };
259           };
260           config.settings = mkMerge [
261             {
262               inherit (config) port logfile databases maxclients appendOnly;
263               daemonize = false;
264               supervised = "systemd";
265               loglevel = config.logLevel;
266               syslog-enabled = config.syslog;
267               save = if config.save == []
268                 then ''""'' # Disable saving with `save = ""`
269                 else map
270                   (d: "${toString (builtins.elemAt d 0)} ${toString (builtins.elemAt d 1)}")
271                   config.save;
272               dbfilename = "dump.rdb";
273               dir = "/var/lib/${redisName name}";
274               appendfsync = config.appendFsync;
275               slowlog-log-slower-than = config.slowLogLogSlowerThan;
276               slowlog-max-len = config.slowLogMaxLen;
277             }
278             (mkIf (config.bind != null) { inherit (config) bind; })
279             (mkIf (config.unixSocket != null) {
280               unixsocket = config.unixSocket;
281               unixsocketperm = toString config.unixSocketPerm;
282             })
283             (mkIf (config.slaveOf != null) { slaveof = "${config.slaveOf.ip} ${toString config.slaveOf.port}"; })
284             (mkIf (config.masterAuth != null) { masterauth = config.masterAuth; })
285             (mkIf (config.requirePass != null) { requirepass = config.requirePass; })
286           ];
287         }));
288         description = "Configuration of multiple `redis-server` instances.";
289         default = {};
290       };
291     };
293   };
296   ###### implementation
298   config = mkIf (enabledServers != {}) {
300     assertions = attrValues (mapAttrs (name: conf: {
301       assertion = conf.requirePass != null -> conf.requirePassFile == null;
302       message = ''
303         You can only set one services.redis.servers.${name}.requirePass
304         or services.redis.servers.${name}.requirePassFile
305       '';
306     }) enabledServers);
308     boot.kernel.sysctl = mkIf cfg.vmOverCommit {
309       "vm.overcommit_memory" = "1";
310     };
312     networking.firewall.allowedTCPPorts = concatMap (conf:
313       optional conf.openFirewall conf.port
314     ) (attrValues enabledServers);
316     environment.systemPackages = [ cfg.package ];
318     users.users = mapAttrs' (name: conf: nameValuePair (redisName name) {
319       description = "System user for the redis-server instance ${name}";
320       isSystemUser = true;
321       group = redisName name;
322     }) enabledServers;
323     users.groups = mapAttrs' (name: conf: nameValuePair (redisName name) {
324     }) enabledServers;
326     systemd.services = mapAttrs' (name: conf: nameValuePair (redisName name) {
327       description = "Redis Server - ${redisName name}";
329       wantedBy = [ "multi-user.target" ];
330       after = [ "network.target" ];
332       serviceConfig = {
333         ExecStart = "${cfg.package}/bin/${cfg.package.serverBin or "redis-server"} /var/lib/${redisName name}/redis.conf ${escapeShellArgs conf.extraParams}";
334         ExecStartPre = "+"+pkgs.writeShellScript "${redisName name}-prep-conf" (let
335           redisConfVar = "/var/lib/${redisName name}/redis.conf";
336           redisConfRun = "/run/${redisName name}/nixos.conf";
337           redisConfStore = redisConfig conf.settings;
338         in ''
339           touch "${redisConfVar}" "${redisConfRun}"
340           chown '${conf.user}' "${redisConfVar}" "${redisConfRun}"
341           chmod 0600 "${redisConfVar}" "${redisConfRun}"
342           if [ ! -s ${redisConfVar} ]; then
343             echo 'include "${redisConfRun}"' > "${redisConfVar}"
344           fi
345           echo 'include "${redisConfStore}"' > "${redisConfRun}"
346           ${optionalString (conf.requirePassFile != null) ''
347             {
348               echo -n "requirepass "
349               cat ${escapeShellArg conf.requirePassFile}
350             } >> "${redisConfRun}"
351           ''}
352         '');
353         Type = "notify";
354         # User and group
355         User = conf.user;
356         Group = conf.user;
357         # Runtime directory and mode
358         RuntimeDirectory = redisName name;
359         RuntimeDirectoryMode = "0750";
360         # State directory and mode
361         StateDirectory = redisName name;
362         StateDirectoryMode = "0700";
363         # Access write directories
364         UMask = "0077";
365         # Capabilities
366         CapabilityBoundingSet = "";
367         # Security
368         NoNewPrivileges = true;
369         # Process Properties
370         LimitNOFILE = mkDefault "${toString (conf.maxclients + 32)}";
371         # Sandboxing
372         ProtectSystem = "strict";
373         ProtectHome = true;
374         PrivateTmp = true;
375         PrivateDevices = true;
376         PrivateUsers = true;
377         ProtectClock = true;
378         ProtectHostname = true;
379         ProtectKernelLogs = true;
380         ProtectKernelModules = true;
381         ProtectKernelTunables = true;
382         ProtectControlGroups = true;
383         RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
384         RestrictNamespaces = true;
385         LockPersonality = true;
386         # we need to disable MemoryDenyWriteExecute for keydb
387         MemoryDenyWriteExecute = cfg.package.pname != "keydb";
388         RestrictRealtime = true;
389         RestrictSUIDSGID = true;
390         PrivateMounts = true;
391         # System Call Filtering
392         SystemCallArchitectures = "native";
393         SystemCallFilter = "~@cpu-emulation @debug @keyring @memlock @mount @obsolete @privileged @resources @setuid";
394       };
395     }) enabledServers;
397   };