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