]> Git — Sourcephile - sourcephile-nix.git/blob - nixos/modules/services/databases/redis.nix
nix: update to nixos-24.11
[sourcephile-nix.git] / nixos / modules / services / databases / redis.nix
1 { config, lib, pkgs, ... }:
2
3 with lib;
4
5 let
6 cfg = config.services.redis;
7
8 mkValueString = value:
9 if value == true then "yes"
10 else if value == false then "no"
11 else generators.mkValueStringDefault { } value;
12
13 redisConfig = settings: pkgs.writeText "redis.conf" (generators.toKeyValue
14 {
15 listsAsDuplicateKeys = true;
16 mkKeyValue = generators.mkKeyValueDefault { inherit mkValueString; } " ";
17 }
18 settings);
19
20 redisName = name: "redis" + optionalString (name != "") ("-" + name);
21 enabledServers = filterAttrs (_name: conf: conf.enable) config.services.redis.servers;
22
23 in
24 {
25 imports = [
26 (mkRemovedOptionModule [ "services" "redis" "user" ] "The redis module now is hardcoded to the redis user.")
27 (mkRemovedOptionModule [ "services" "redis" "dbpath" ] "The redis module now uses /var/lib/redis as data directory.")
28 (mkRemovedOptionModule [ "services" "redis" "dbFilename" ] "The redis module now uses /var/lib/redis/dump.rdb as database dump location.")
29 (mkRemovedOptionModule [ "services" "redis" "appendOnlyFilename" ] "This option was never used.")
30 (mkRemovedOptionModule [ "services" "redis" "pidFile" ] "This option was removed.")
31 (mkRemovedOptionModule [ "services" "redis" "extraConfig" ] "Use services.redis.settings instead.")
32 (mkRenamedOptionModule [ "services" "redis" "enable" ] [ "services" "redis" "servers" "" "enable" ])
33 (mkRenamedOptionModule [ "services" "redis" "port" ] [ "services" "redis" "servers" "" "port" ])
34 (mkRenamedOptionModule [ "services" "redis" "openFirewall" ] [ "services" "redis" "servers" "" "openFirewall" ])
35 (mkRenamedOptionModule [ "services" "redis" "bind" ] [ "services" "redis" "servers" "" "bind" ])
36 (mkRenamedOptionModule [ "services" "redis" "unixSocket" ] [ "services" "redis" "servers" "" "unixSocket" ])
37 (mkRenamedOptionModule [ "services" "redis" "unixSocketPerm" ] [ "services" "redis" "servers" "" "unixSocketPerm" ])
38 (mkRenamedOptionModule [ "services" "redis" "logLevel" ] [ "services" "redis" "servers" "" "logLevel" ])
39 (mkRenamedOptionModule [ "services" "redis" "logfile" ] [ "services" "redis" "servers" "" "logfile" ])
40 (mkRenamedOptionModule [ "services" "redis" "syslog" ] [ "services" "redis" "servers" "" "syslog" ])
41 (mkRenamedOptionModule [ "services" "redis" "databases" ] [ "services" "redis" "servers" "" "databases" ])
42 (mkRenamedOptionModule [ "services" "redis" "maxclients" ] [ "services" "redis" "servers" "" "maxclients" ])
43 (mkRenamedOptionModule [ "services" "redis" "save" ] [ "services" "redis" "servers" "" "save" ])
44 (mkRenamedOptionModule [ "services" "redis" "slaveOf" ] [ "services" "redis" "servers" "" "slaveOf" ])
45 (mkRenamedOptionModule [ "services" "redis" "masterAuth" ] [ "services" "redis" "servers" "" "masterAuth" ])
46 (mkRenamedOptionModule [ "services" "redis" "requirePass" ] [ "services" "redis" "servers" "" "requirePass" ])
47 (mkRenamedOptionModule [ "services" "redis" "requirePassFile" ] [ "services" "redis" "servers" "" "requirePassFile" ])
48 (mkRenamedOptionModule [ "services" "redis" "appendOnly" ] [ "services" "redis" "servers" "" "appendOnly" ])
49 (mkRenamedOptionModule [ "services" "redis" "appendFsync" ] [ "services" "redis" "servers" "" "appendFsync" ])
50 (mkRenamedOptionModule [ "services" "redis" "slowLogLogSlowerThan" ] [ "services" "redis" "servers" "" "slowLogLogSlowerThan" ])
51 (mkRenamedOptionModule [ "services" "redis" "slowLogMaxLen" ] [ "services" "redis" "servers" "" "slowLogMaxLen" ])
52 (mkRenamedOptionModule [ "services" "redis" "settings" ] [ "services" "redis" "servers" "" "settings" ])
53 ];
54
55 ###### interface
56
57 options = {
58
59 services.redis = {
60 package = mkOption {
61 type = types.package;
62 default = pkgs.redis;
63 defaultText = "pkgs.redis";
64 description = "Which Redis derivation to use.";
65 };
66
67 vmOverCommit = mkEnableOption ''
68 setting of vm.overcommit_memory to 1
69 (Suggested for Background Saving: http://redis.io/topics/faq)
70 '';
71
72 servers = mkOption {
73 type = with types; attrsOf (submodule ({ config, name, ... }: {
74 options = {
75 enable = mkEnableOption ''
76 Redis server.
77
78 Note that the NixOS module for Redis disables kernel support
79 for Transparent Huge Pages (THP),
80 because this features causes major performance problems for Redis,
81 e.g. (https://redis.io/topics/latency).
82 '';
83
84 user = mkOption {
85 type = types.str;
86 default = redisName name;
87 defaultText = "\"redis\" or \"redis-\${name}\" if name != \"\"";
88 description = "The username and groupname for redis-server.";
89 };
90
91 port = mkOption {
92 type = types.port;
93 default = 6379;
94 description = "The port for Redis to listen to.";
95 };
96
97 openFirewall = mkOption {
98 type = types.bool;
99 default = false;
100 description = ''
101 Whether to open ports in the firewall for the server.
102 '';
103 };
104
105 bind = mkOption {
106 type = with types; nullOr str;
107 default = if name == "" then "127.0.0.1" else null;
108 defaultText = "127.0.0.1 or null if name != \"\"";
109 description = ''
110 The IP interface to bind to.
111 <literal>null</literal> means "all interfaces".
112 '';
113 example = "192.0.2.1";
114 };
115
116 unixSocket = mkOption {
117 type = with types; nullOr path;
118 default = "/run/${redisName name}/redis.sock";
119 defaultText = "\"/run/redis/redis.sock\" or \"/run/redis-\${name}/redis.sock\" if name != \"\"";
120 description = "The path to the socket to bind to.";
121 };
122
123 unixSocketPerm = mkOption {
124 type = types.int;
125 default = 660;
126 description = "Change permissions for the socket";
127 example = 600;
128 };
129
130 logLevel = mkOption {
131 type = types.str;
132 default = "notice"; # debug, verbose, notice, warning
133 example = "debug";
134 description = "Specify the server verbosity level, options: debug, verbose, notice, warning.";
135 };
136
137 logfile = mkOption {
138 type = types.str;
139 default = "/dev/null";
140 description = "Specify the log file name. Also 'stdout' can be used to force Redis to log on the standard output.";
141 example = "/var/log/redis.log";
142 };
143
144 syslog = mkOption {
145 type = types.bool;
146 default = true;
147 description = "Enable logging to the system logger.";
148 };
149
150 databases = mkOption {
151 type = types.int;
152 default = 16;
153 description = "Set the number of databases.";
154 };
155
156 maxclients = mkOption {
157 type = types.int;
158 default = 10000;
159 description = "Set the max number of connected clients at the same time.";
160 };
161
162 save = mkOption {
163 type = with types; listOf (listOf int);
164 default = [ [ 900 1 ] [ 300 10 ] [ 60 10000 ] ];
165 description = "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.";
166 example = [ [ 900 1 ] [ 300 10 ] [ 60 10000 ] ];
167 };
168
169 slaveOf = mkOption {
170 type = with types; nullOr (submodule ({ ... }: {
171 options = {
172 ip = mkOption {
173 type = str;
174 description = "IP of the Redis master";
175 example = "192.168.1.100";
176 };
177
178 port = mkOption {
179 type = port;
180 description = "port of the Redis master";
181 default = 6379;
182 };
183 };
184 }));
185
186 default = null;
187 description = "IP and port to which this redis instance acts as a slave.";
188 example = { ip = "192.168.1.100"; port = 6379; };
189 };
190
191 masterAuth = mkOption {
192 type = with types; nullOr str;
193 default = null;
194 description = ''If the master is password protected (using the requirePass configuration)
195 it is possible to tell the slave to authenticate before starting the replication synchronization
196 process, otherwise the master will refuse the slave request.
197 (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE)'';
198 };
199
200 requirePass = mkOption {
201 type = with types; nullOr str;
202 default = null;
203 description = ''
204 Password for database (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE).
205 Use requirePassFile to store it outside of the nix store in a dedicated file.
206 '';
207 example = "letmein!";
208 };
209
210 requirePassFile = mkOption {
211 type = with types; nullOr path;
212 default = null;
213 description = "File with password for the database.";
214 example = "/run/keys/redis-password";
215 };
216
217 appendOnly = mkOption {
218 type = types.bool;
219 default = false;
220 description = "By default data is only periodically persisted to disk, enable this option to use an append-only file for improved persistence.";
221 };
222
223 appendFsync = mkOption {
224 type = types.str;
225 default = "everysec"; # no, always, everysec
226 description = "How often to fsync the append-only log, options: no, always, everysec.";
227 };
228
229 slowLogLogSlowerThan = mkOption {
230 type = types.int;
231 default = 10000;
232 description = "Log queries whose execution take longer than X in milliseconds.";
233 example = 1000;
234 };
235
236 slowLogMaxLen = mkOption {
237 type = types.int;
238 default = 128;
239 description = "Maximum number of items to keep in slow log.";
240 };
241
242 settings = mkOption {
243 # TODO: this should be converted to freeformType
244 type = with types; attrsOf (oneOf [ bool int str (listOf str) ]);
245 default = { };
246 description = ''
247 Redis configuration. Refer to
248 <link xlink:href="https://redis.io/topics/config"/>
249 for details on supported values.
250 '';
251 example = literalExample ''
252 {
253 loadmodule = [ "/path/to/my_module.so" "/path/to/other_module.so" ];
254 }
255 '';
256 };
257 };
258 config.settings = mkMerge [
259 {
260 port = if config.bind == null then 0 else config.port;
261 daemonize = false;
262 supervised = "systemd";
263 loglevel = config.logLevel;
264 logfile = config.logfile;
265 syslog-enabled = config.syslog;
266 databases = config.databases;
267 maxclients = config.maxclients;
268 save = map (d: "${toString (builtins.elemAt d 0)} ${toString (builtins.elemAt d 1)}") config.save;
269 dbfilename = "dump.rdb";
270 dir = "/var/lib/${redisName name}";
271 appendOnly = config.appendOnly;
272 appendfsync = config.appendFsync;
273 slowlog-log-slower-than = config.slowLogLogSlowerThan;
274 slowlog-max-len = config.slowLogMaxLen;
275 }
276 (mkIf (config.bind != null) { bind = config.bind; })
277 (mkIf (config.unixSocket != null) {
278 unixsocket = config.unixSocket;
279 unixsocketperm = toString config.unixSocketPerm;
280 })
281 (mkIf (config.slaveOf != null) { slaveof = "${config.slaveOf.ip} ${toString config.slaveOf.port}"; })
282 (mkIf (config.masterAuth != null) { masterauth = config.masterAuth; })
283 (mkIf (config.requirePass != null) { requirepass = config.requirePass; })
284 ];
285 }));
286 description = "Configuration of multiple <literal>redis-server</literal> instances.";
287 default = { };
288 };
289 };
290
291 };
292
293
294 ###### implementation
295
296 config = mkIf (enabledServers != { }) {
297
298 assertions = attrValues (mapAttrs
299 (name: conf: {
300 assertion = conf.requirePass != null -> conf.requirePassFile == null;
301 message = ''
302 You can only set one services.redis.servers.${name}.requirePass
303 or services.redis.servers.${name}.requirePassFile
304 '';
305 })
306 enabledServers);
307
308 boot.kernel.sysctl = mkMerge [
309 { "vm.nr_hugepages" = "0"; }
310 (mkIf cfg.vmOverCommit { "vm.overcommit_memory" = "1"; })
311 ];
312
313 networking.firewall.allowedTCPPorts = concatMap
314 (conf:
315 optional conf.openFirewall conf.port
316 )
317 (attrValues enabledServers);
318
319 environment.systemPackages = [ cfg.package ];
320
321 users.users = mapAttrs'
322 (name: _conf: nameValuePair (redisName name) {
323 description = "System user for the redis-server instance ${name}";
324 isSystemUser = true;
325 group = redisName name;
326 })
327 enabledServers;
328 users.groups = mapAttrs'
329 (name: _conf: nameValuePair (redisName name) { })
330 enabledServers;
331
332 systemd.services = mapAttrs'
333 (name: conf: nameValuePair (redisName name) {
334 description = "Redis Server - ${redisName name}";
335
336 wantedBy = [ "multi-user.target" ];
337 after = [ "network.target" ];
338
339 serviceConfig = {
340 ExecStart = "${cfg.package}/bin/redis-server /run/${redisName name}/redis.conf";
341 ExecStartPre = [
342 ("+" + pkgs.writeShellScript "${redisName name}-credentials" (''
343 install -o '${conf.user}' -m 600 ${redisConfig conf.settings} /run/${redisName name}/redis.conf
344 '' + optionalString (conf.requirePassFile != null) ''
345 {
346 printf requirePass' '
347 cat ${escapeShellArg conf.requirePassFile}
348 } >>/run/${redisName name}/redis.conf
349 '')
350 )
351 ];
352 Type = "notify";
353 # User and group
354 User = conf.user;
355 Group = conf.user;
356 # Runtime directory and mode
357 RuntimeDirectory = redisName name;
358 RuntimeDirectoryMode = "0750";
359 # State directory and mode
360 StateDirectory = redisName name;
361 StateDirectoryMode = "0700";
362 # Access write directories
363 UMask = "0077";
364 # Capabilities
365 CapabilityBoundingSet = "";
366 # Security
367 NoNewPrivileges = true;
368 # Process Properties
369 LimitNOFILE = mkDefault "${toString (conf.maxclients + 32)}";
370 # Sandboxing
371 ProtectSystem = "strict";
372 ProtectHome = true;
373 PrivateTmp = true;
374 PrivateDevices = true;
375 PrivateUsers = true;
376 ProtectClock = true;
377 ProtectHostname = true;
378 ProtectKernelLogs = true;
379 ProtectKernelModules = true;
380 ProtectKernelTunables = true;
381 ProtectControlGroups = true;
382 RestrictAddressFamilies =
383 optionals (conf.bind != null) [ "AF_INET" "AF_INET6" ] ++
384 optional (conf.unixSocket != null) "AF_UNIX";
385 RestrictNamespaces = true;
386 LockPersonality = true;
387 MemoryDenyWriteExecute = true;
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 })
396 enabledServers;
397
398 };
399 }