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, ... }: {
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 inherit (config) port logfile databases maxclients appendOnly;
275 daemonize = false;
276 supervised = "systemd";
277 loglevel = config.logLevel;
278 syslog-enabled = config.syslog;
279 save = if config.save == []
280 then ''""'' # Disable saving with `save = ""`
281 else map
282 (d: "${toString (builtins.elemAt d 0)} ${toString (builtins.elemAt d 1)}")
283 config.save;
284 dbfilename = "dump.rdb";
285 dir = "/var/lib/${redisName name}";
286 appendfsync = config.appendFsync;
287 slowlog-log-slower-than = config.slowLogLogSlowerThan;
288 slowlog-max-len = config.slowLogMaxLen;
289 }
290 (mkIf (config.bind != null) { inherit (config) bind; })
291 (mkIf (config.unixSocket != null) {
292 unixsocket = config.unixSocket;
293 unixsocketperm = toString config.unixSocketPerm;
294 })
295 (mkIf (config.slaveOf != null) { slaveof = "${config.slaveOf.ip} ${toString config.slaveOf.port}"; })
296 (mkIf (config.masterAuth != null) { masterauth = config.masterAuth; })
297 (mkIf (config.requirePass != null) { requirepass = config.requirePass; })
298 ];
299 }));
300 description = lib.mdDoc "Configuration of multiple `redis-server` instances.";
301 default = {};
302 };
303 };
304
305 };
306
307
308 ###### implementation
309
310 config = mkIf (enabledServers != {}) {
311
312 assertions = attrValues (mapAttrs (name: conf: {
313 assertion = conf.requirePass != null -> conf.requirePassFile == null;
314 message = ''
315 You can only set one services.redis.servers.${name}.requirePass
316 or services.redis.servers.${name}.requirePassFile
317 '';
318 }) enabledServers);
319
320 boot.kernel.sysctl = mkMerge [
321 { "vm.nr_hugepages" = "0"; }
322 ( mkIf cfg.vmOverCommit { "vm.overcommit_memory" = "1"; } )
323 ];
324
325 networking.firewall.allowedTCPPorts = concatMap (conf:
326 optional conf.openFirewall conf.port
327 ) (attrValues enabledServers);
328
329 environment.systemPackages = [ cfg.package ];
330
331 users.users = mapAttrs' (name: conf: nameValuePair (redisName name) {
332 description = "System user for the redis-server instance ${name}";
333 isSystemUser = true;
334 group = redisName name;
335 }) enabledServers;
336 users.groups = mapAttrs' (name: conf: nameValuePair (redisName name) {
337 }) enabledServers;
338
339 systemd.services = mapAttrs' (name: conf: nameValuePair (redisName name) {
340 description = "Redis Server - ${redisName name}";
341
342 wantedBy = [ "multi-user.target" ];
343 after = [ "network.target" ];
344
345 serviceConfig = {
346 ExecStart = "${cfg.package}/bin/redis-server /var/lib/${redisName name}/redis.conf ${escapeShellArgs conf.extraParams}";
347 ExecStartPre = "+"+pkgs.writeShellScript "${redisName name}-prep-conf" (let
348 redisConfVar = "/var/lib/${redisName name}/redis.conf";
349 redisConfRun = "/run/${redisName name}/nixos.conf";
350 redisConfStore = redisConfig conf.settings;
351 in ''
352 touch "${redisConfVar}" "${redisConfRun}"
353 chown '${conf.user}' "${redisConfVar}" "${redisConfRun}"
354 chmod 0600 "${redisConfVar}" "${redisConfRun}"
355 if [ ! -s ${redisConfVar} ]; then
356 echo 'include "${redisConfRun}"' > "${redisConfVar}"
357 fi
358 echo 'include "${redisConfStore}"' > "${redisConfRun}"
359 ${optionalString (conf.requirePassFile != null) ''
360 {
361 echo -n "requirepass "
362 cat ${escapeShellArg conf.requirePassFile}
363 } >> "${redisConfRun}"
364 ''}
365 '');
366 Type = "notify";
367 # User and group
368 User = conf.user;
369 Group = conf.user;
370 # Runtime directory and mode
371 RuntimeDirectory = redisName name;
372 RuntimeDirectoryMode = "0750";
373 # State directory and mode
374 StateDirectory = redisName name;
375 StateDirectoryMode = "0700";
376 # Access write directories
377 UMask = "0077";
378 # Capabilities
379 CapabilityBoundingSet = "";
380 # Security
381 NoNewPrivileges = true;
382 # Process Properties
383 LimitNOFILE = mkDefault "${toString (conf.maxclients + 32)}";
384 # Sandboxing
385 ProtectSystem = "strict";
386 ProtectHome = true;
387 PrivateTmp = true;
388 PrivateDevices = true;
389 PrivateUsers = true;
390 ProtectClock = true;
391 ProtectHostname = true;
392 ProtectKernelLogs = true;
393 ProtectKernelModules = true;
394 ProtectKernelTunables = true;
395 ProtectControlGroups = true;
396 RestrictAddressFamilies =
397 optionals (conf.port != 0) ["AF_INET" "AF_INET6"] ++
398 optional (conf.unixSocket != null) "AF_UNIX";
399 RestrictNamespaces = true;
400 LockPersonality = true;
401 MemoryDenyWriteExecute = true;
402 RestrictRealtime = true;
403 RestrictSUIDSGID = true;
404 PrivateMounts = true;
405 # System Call Filtering
406 SystemCallArchitectures = "native";
407 SystemCallFilter = "~@cpu-emulation @debug @keyring @memlock @mount @obsolete @privileged @resources @setuid";
408 };
409 }) enabledServers;
410
411 };
412}