1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 cfg = config.services.postgresql;
8
9 postgresql =
10 let
11 # ensure that
12 # services.postgresql = {
13 # enableJIT = true;
14 # package = pkgs.postgresql_<major>;
15 # };
16 # works.
17 base = if cfg.enableJIT && !cfg.package.jitSupport then cfg.package.withJIT else cfg.package;
18 in
19 if cfg.extraPlugins == []
20 then base
21 else base.withPackages (_: cfg.extraPlugins);
22
23 toStr = value:
24 if true == value then "yes"
25 else if false == value then "no"
26 else if isString value then "'${lib.replaceStrings ["'"] ["''"] value}'"
27 else toString value;
28
29 # The main PostgreSQL configuration file.
30 configFile = pkgs.writeTextDir "postgresql.conf" (concatStringsSep "\n" (mapAttrsToList (n: v: "${n} = ${toStr v}") cfg.settings));
31
32 configFileCheck = pkgs.runCommand "postgresql-configfile-check" {} ''
33 ${cfg.package}/bin/postgres -D${configFile} -C config_file >/dev/null
34 touch $out
35 '';
36
37 groupAccessAvailable = versionAtLeast postgresql.version "11.0";
38
39in
40
41{
42 imports = [
43 (mkRemovedOptionModule [ "services" "postgresql" "extraConfig" ] "Use services.postgresql.settings instead.")
44 ];
45
46 ###### interface
47
48 options = {
49
50 services.postgresql = {
51
52 enable = mkEnableOption (lib.mdDoc "PostgreSQL Server");
53
54 enableJIT = mkEnableOption (lib.mdDoc "JIT support");
55
56 package = mkOption {
57 type = types.package;
58 example = literalExpression "pkgs.postgresql_11";
59 description = lib.mdDoc ''
60 PostgreSQL package to use.
61 '';
62 };
63
64 port = mkOption {
65 type = types.port;
66 default = 5432;
67 description = lib.mdDoc ''
68 The port on which PostgreSQL listens.
69 '';
70 };
71
72 checkConfig = mkOption {
73 type = types.bool;
74 default = true;
75 description = lib.mdDoc "Check the syntax of the configuration file at compile time";
76 };
77
78 dataDir = mkOption {
79 type = types.path;
80 defaultText = literalExpression ''"/var/lib/postgresql/''${config.services.postgresql.package.psqlSchema}"'';
81 example = "/var/lib/postgresql/11";
82 description = lib.mdDoc ''
83 The data directory for PostgreSQL. If left as the default value
84 this directory will automatically be created before the PostgreSQL server starts, otherwise
85 the sysadmin is responsible for ensuring the directory exists with appropriate ownership
86 and permissions.
87 '';
88 };
89
90 authentication = mkOption {
91 type = types.lines;
92 default = "";
93 description = lib.mdDoc ''
94 Defines how users authenticate themselves to the server. See the
95 [PostgreSQL documentation for pg_hba.conf](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html)
96 for details on the expected format of this option. By default,
97 peer based authentication will be used for users connecting
98 via the Unix socket, and md5 password authentication will be
99 used for users connecting via TCP. Any added rules will be
100 inserted above the default rules. If you'd like to replace the
101 default rules entirely, you can use `lib.mkForce` in your
102 module.
103 '';
104 };
105
106 identMap = mkOption {
107 type = types.lines;
108 default = "";
109 description = lib.mdDoc ''
110 Defines the mapping from system users to database users.
111
112 The general form is:
113
114 map-name system-username database-username
115 '';
116 };
117
118 initdbArgs = mkOption {
119 type = with types; listOf str;
120 default = [];
121 example = [ "--data-checksums" "--allow-group-access" ];
122 description = lib.mdDoc ''
123 Additional arguments passed to `initdb` during data dir
124 initialisation.
125 '';
126 };
127
128 initialScript = mkOption {
129 type = types.nullOr types.path;
130 default = null;
131 description = lib.mdDoc ''
132 A file containing SQL statements to execute on first startup.
133 '';
134 };
135
136 ensureDatabases = mkOption {
137 type = types.listOf types.str;
138 default = [];
139 description = lib.mdDoc ''
140 Ensures that the specified databases exist.
141 This option will never delete existing databases, especially not when the value of this
142 option is changed. This means that databases created once through this option or
143 otherwise have to be removed manually.
144 '';
145 example = [
146 "gitea"
147 "nextcloud"
148 ];
149 };
150
151 ensureUsers = mkOption {
152 type = types.listOf (types.submodule {
153 options = {
154 name = mkOption {
155 type = types.str;
156 description = lib.mdDoc ''
157 Name of the user to ensure.
158 '';
159 };
160
161 ensurePermissions = mkOption {
162 type = types.attrsOf types.str;
163 default = {};
164 description = lib.mdDoc ''
165 Permissions to ensure for the user, specified as an attribute set.
166 The attribute names specify the database and tables to grant the permissions for.
167 The attribute values specify the permissions to grant. You may specify one or
168 multiple comma-separated SQL privileges here.
169
170 For more information on how to specify the target
171 and on which privileges exist, see the
172 [GRANT syntax](https://www.postgresql.org/docs/current/sql-grant.html).
173 The attributes are used as `GRANT ''${attrValue} ON ''${attrName}`.
174 '';
175 example = literalExpression ''
176 {
177 "DATABASE \"nextcloud\"" = "ALL PRIVILEGES";
178 "ALL TABLES IN SCHEMA public" = "ALL PRIVILEGES";
179 }
180 '';
181 };
182
183 ensureClauses = mkOption {
184 description = lib.mdDoc ''
185 An attrset of clauses to grant to the user. Under the hood this uses the
186 [ALTER USER syntax](https://www.postgresql.org/docs/current/sql-alteruser.html) for each attrName where
187 the attrValue is true in the attrSet:
188 `ALTER USER user.name WITH attrName`
189 '';
190 example = literalExpression ''
191 {
192 superuser = true;
193 createrole = true;
194 createdb = true;
195 }
196 '';
197 default = {};
198 defaultText = lib.literalMD ''
199 The default, `null`, means that the user created will have the default permissions assigned by PostgreSQL. Subsequent server starts will not set or unset the clause, so imperative changes are preserved.
200 '';
201 type = types.submodule {
202 options = let
203 defaultText = lib.literalMD ''
204 `null`: do not set. For newly created roles, use PostgreSQL's default. For existing roles, do not touch this clause.
205 '';
206 in {
207 superuser = mkOption {
208 type = types.nullOr types.bool;
209 description = lib.mdDoc ''
210 Grants the user, created by the ensureUser attr, superuser permissions. From the postgres docs:
211
212 A database superuser bypasses all permission checks,
213 except the right to log in. This is a dangerous privilege
214 and should not be used carelessly; it is best to do most
215 of your work as a role that is not a superuser. To create
216 a new database superuser, use CREATE ROLE name SUPERUSER.
217 You must do this as a role that is already a superuser.
218
219 More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
220 '';
221 default = null;
222 inherit defaultText;
223 };
224 createrole = mkOption {
225 type = types.nullOr types.bool;
226 description = lib.mdDoc ''
227 Grants the user, created by the ensureUser attr, createrole permissions. From the postgres docs:
228
229 A role must be explicitly given permission to create more
230 roles (except for superusers, since those bypass all
231 permission checks). To create such a role, use CREATE
232 ROLE name CREATEROLE. A role with CREATEROLE privilege
233 can alter and drop other roles, too, as well as grant or
234 revoke membership in them. However, to create, alter,
235 drop, or change membership of a superuser role, superuser
236 status is required; CREATEROLE is insufficient for that.
237
238 More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
239 '';
240 default = null;
241 inherit defaultText;
242 };
243 createdb = mkOption {
244 type = types.nullOr types.bool;
245 description = lib.mdDoc ''
246 Grants the user, created by the ensureUser attr, createdb permissions. From the postgres docs:
247
248 A role must be explicitly given permission to create
249 databases (except for superusers, since those bypass all
250 permission checks). To create such a role, use CREATE
251 ROLE name CREATEDB.
252
253 More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
254 '';
255 default = null;
256 inherit defaultText;
257 };
258 "inherit" = mkOption {
259 type = types.nullOr types.bool;
260 description = lib.mdDoc ''
261 Grants the user created inherit permissions. From the postgres docs:
262
263 A role is given permission to inherit the privileges of
264 roles it is a member of, by default. However, to create a
265 role without the permission, use CREATE ROLE name
266 NOINHERIT.
267
268 More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
269 '';
270 default = null;
271 inherit defaultText;
272 };
273 login = mkOption {
274 type = types.nullOr types.bool;
275 description = lib.mdDoc ''
276 Grants the user, created by the ensureUser attr, login permissions. From the postgres docs:
277
278 Only roles that have the LOGIN attribute can be used as
279 the initial role name for a database connection. A role
280 with the LOGIN attribute can be considered the same as a
281 “database user”. To create a role with login privilege,
282 use either:
283
284 CREATE ROLE name LOGIN; CREATE USER name;
285
286 (CREATE USER is equivalent to CREATE ROLE except that
287 CREATE USER includes LOGIN by default, while CREATE ROLE
288 does not.)
289
290 More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
291 '';
292 default = null;
293 inherit defaultText;
294 };
295 replication = mkOption {
296 type = types.nullOr types.bool;
297 description = lib.mdDoc ''
298 Grants the user, created by the ensureUser attr, replication permissions. From the postgres docs:
299
300 A role must explicitly be given permission to initiate
301 streaming replication (except for superusers, since those
302 bypass all permission checks). A role used for streaming
303 replication must have LOGIN permission as well. To create
304 such a role, use CREATE ROLE name REPLICATION LOGIN.
305
306 More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
307 '';
308 default = null;
309 inherit defaultText;
310 };
311 bypassrls = mkOption {
312 type = types.nullOr types.bool;
313 description = lib.mdDoc ''
314 Grants the user, created by the ensureUser attr, replication permissions. From the postgres docs:
315
316 A role must be explicitly given permission to bypass
317 every row-level security (RLS) policy (except for
318 superusers, since those bypass all permission checks). To
319 create such a role, use CREATE ROLE name BYPASSRLS as a
320 superuser.
321
322 More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
323 '';
324 default = null;
325 inherit defaultText;
326 };
327 };
328 };
329 };
330 };
331 });
332 default = [];
333 description = lib.mdDoc ''
334 Ensures that the specified users exist and have at least the ensured permissions.
335 The PostgreSQL users will be identified using peer authentication. This authenticates the Unix user with the
336 same name only, and that without the need for a password.
337 This option will never delete existing users or remove permissions, especially not when the value of this
338 option is changed. This means that users created and permissions assigned once through this option or
339 otherwise have to be removed manually.
340 '';
341 example = literalExpression ''
342 [
343 {
344 name = "nextcloud";
345 ensurePermissions = {
346 "DATABASE nextcloud" = "ALL PRIVILEGES";
347 };
348 }
349 {
350 name = "superuser";
351 ensurePermissions = {
352 "ALL TABLES IN SCHEMA public" = "ALL PRIVILEGES";
353 };
354 }
355 ]
356 '';
357 };
358
359 enableTCPIP = mkOption {
360 type = types.bool;
361 default = false;
362 description = lib.mdDoc ''
363 Whether PostgreSQL should listen on all network interfaces.
364 If disabled, the database can only be accessed via its Unix
365 domain socket or via TCP connections to localhost.
366 '';
367 };
368
369 logLinePrefix = mkOption {
370 type = types.str;
371 default = "[%p] ";
372 example = "%m [%p] ";
373 description = lib.mdDoc ''
374 A printf-style string that is output at the beginning of each log line.
375 Upstream default is `'%m [%p] '`, i.e. it includes the timestamp. We do
376 not include the timestamp, because journal has it anyway.
377 '';
378 };
379
380 extraPlugins = mkOption {
381 type = types.listOf types.path;
382 default = [];
383 example = literalExpression "with pkgs.postgresql_11.pkgs; [ postgis pg_repack ]";
384 description = lib.mdDoc ''
385 List of PostgreSQL plugins. PostgreSQL version for each plugin should
386 match version for `services.postgresql.package` value.
387 '';
388 };
389
390 settings = mkOption {
391 type = with types; attrsOf (oneOf [ bool float int str ]);
392 default = {};
393 description = lib.mdDoc ''
394 PostgreSQL configuration. Refer to
395 <https://www.postgresql.org/docs/11/config-setting.html#CONFIG-SETTING-CONFIGURATION-FILE>
396 for an overview of `postgresql.conf`.
397
398 ::: {.note}
399 String values will automatically be enclosed in single quotes. Single quotes will be
400 escaped with two single quotes as described by the upstream documentation linked above.
401 :::
402 '';
403 example = literalExpression ''
404 {
405 log_connections = true;
406 log_statement = "all";
407 logging_collector = true
408 log_disconnections = true
409 log_destination = lib.mkForce "syslog";
410 }
411 '';
412 };
413
414 recoveryConfig = mkOption {
415 type = types.nullOr types.lines;
416 default = null;
417 description = lib.mdDoc ''
418 Contents of the {file}`recovery.conf` file.
419 '';
420 };
421
422 superUser = mkOption {
423 type = types.str;
424 default = "postgres";
425 internal = true;
426 readOnly = true;
427 description = lib.mdDoc ''
428 PostgreSQL superuser account to use for various operations. Internal since changing
429 this value would lead to breakage while setting up databases.
430 '';
431 };
432 };
433
434 };
435
436
437 ###### implementation
438
439 config = mkIf cfg.enable {
440
441 services.postgresql.settings =
442 {
443 hba_file = "${pkgs.writeText "pg_hba.conf" cfg.authentication}";
444 ident_file = "${pkgs.writeText "pg_ident.conf" cfg.identMap}";
445 log_destination = "stderr";
446 log_line_prefix = cfg.logLinePrefix;
447 listen_addresses = if cfg.enableTCPIP then "*" else "localhost";
448 port = cfg.port;
449 jit = mkDefault (if cfg.enableJIT then "on" else "off");
450 };
451
452 services.postgresql.package = let
453 mkThrow = ver: throw "postgresql_${ver} was removed, please upgrade your postgresql version.";
454 base = if versionAtLeast config.system.stateVersion "22.05" then pkgs.postgresql_14
455 else if versionAtLeast config.system.stateVersion "21.11" then pkgs.postgresql_13
456 else if versionAtLeast config.system.stateVersion "20.03" then pkgs.postgresql_11
457 else if versionAtLeast config.system.stateVersion "17.09" then mkThrow "9_6"
458 else mkThrow "9_5";
459 in
460 # Note: when changing the default, make it conditional on
461 # ‘system.stateVersion’ to maintain compatibility with existing
462 # systems!
463 mkDefault (if cfg.enableJIT then base.withJIT else base);
464
465 services.postgresql.dataDir = mkDefault "/var/lib/postgresql/${cfg.package.psqlSchema}";
466
467 services.postgresql.authentication = mkAfter
468 ''
469 # Generated file; do not edit!
470 local all all peer
471 host all all 127.0.0.1/32 md5
472 host all all ::1/128 md5
473 '';
474
475 users.users.postgres =
476 { name = "postgres";
477 uid = config.ids.uids.postgres;
478 group = "postgres";
479 description = "PostgreSQL server user";
480 home = "${cfg.dataDir}";
481 useDefaultShell = true;
482 };
483
484 users.groups.postgres.gid = config.ids.gids.postgres;
485
486 environment.systemPackages = [ postgresql ];
487
488 environment.pathsToLink = [
489 "/share/postgresql"
490 ];
491
492 system.checks = lib.optional (cfg.checkConfig && pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) configFileCheck;
493
494 systemd.services.postgresql =
495 { description = "PostgreSQL Server";
496
497 wantedBy = [ "multi-user.target" ];
498 after = [ "network.target" ];
499
500 environment.PGDATA = cfg.dataDir;
501
502 path = [ postgresql ];
503
504 preStart =
505 ''
506 if ! test -e ${cfg.dataDir}/PG_VERSION; then
507 # Cleanup the data directory.
508 rm -f ${cfg.dataDir}/*.conf
509
510 # Initialise the database.
511 initdb -U ${cfg.superUser} ${concatStringsSep " " cfg.initdbArgs}
512
513 # See postStart!
514 touch "${cfg.dataDir}/.first_startup"
515 fi
516
517 ln -sfn "${configFile}/postgresql.conf" "${cfg.dataDir}/postgresql.conf"
518 ${optionalString (cfg.recoveryConfig != null) ''
519 ln -sfn "${pkgs.writeText "recovery.conf" cfg.recoveryConfig}" \
520 "${cfg.dataDir}/recovery.conf"
521 ''}
522 '';
523
524 # Wait for PostgreSQL to be ready to accept connections.
525 postStart =
526 ''
527 PSQL="psql --port=${toString cfg.port}"
528
529 while ! $PSQL -d postgres -c "" 2> /dev/null; do
530 if ! kill -0 "$MAINPID"; then exit 1; fi
531 sleep 0.1
532 done
533
534 if test -e "${cfg.dataDir}/.first_startup"; then
535 ${optionalString (cfg.initialScript != null) ''
536 $PSQL -f "${cfg.initialScript}" -d postgres
537 ''}
538 rm -f "${cfg.dataDir}/.first_startup"
539 fi
540 '' + optionalString (cfg.ensureDatabases != []) ''
541 ${concatMapStrings (database: ''
542 $PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${database}'" | grep -q 1 || $PSQL -tAc 'CREATE DATABASE "${database}"'
543 '') cfg.ensureDatabases}
544 '' + ''
545 ${
546 concatMapStrings
547 (user:
548 let
549 userPermissions = concatStringsSep "\n"
550 (mapAttrsToList
551 (database: permission: ''$PSQL -tAc 'GRANT ${permission} ON ${database} TO "${user.name}"' '')
552 user.ensurePermissions
553 );
554
555 filteredClauses = filterAttrs (name: value: value != null) user.ensureClauses;
556
557 clauseSqlStatements = attrValues (mapAttrs (n: v: if v then n else "no${n}") filteredClauses);
558
559 userClauses = ''$PSQL -tAc 'ALTER ROLE "${user.name}" ${concatStringsSep " " clauseSqlStatements}' '';
560 in ''
561 $PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname='${user.name}'" | grep -q 1 || $PSQL -tAc 'CREATE USER "${user.name}"'
562 ${userPermissions}
563 ${userClauses}
564 ''
565 )
566 cfg.ensureUsers
567 }
568 '';
569
570 serviceConfig = mkMerge [
571 { ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
572 User = "postgres";
573 Group = "postgres";
574 RuntimeDirectory = "postgresql";
575 Type = if versionAtLeast cfg.package.version "9.6"
576 then "notify"
577 else "simple";
578
579 # Shut down Postgres using SIGINT ("Fast Shutdown mode"). See
580 # http://www.postgresql.org/docs/current/static/server-shutdown.html
581 KillSignal = "SIGINT";
582 KillMode = "mixed";
583
584 # Give Postgres a decent amount of time to clean up after
585 # receiving systemd's SIGINT.
586 TimeoutSec = 120;
587
588 ExecStart = "${postgresql}/bin/postgres";
589 }
590 (mkIf (cfg.dataDir == "/var/lib/postgresql/${cfg.package.psqlSchema}") {
591 StateDirectory = "postgresql postgresql/${cfg.package.psqlSchema}";
592 StateDirectoryMode = if groupAccessAvailable then "0750" else "0700";
593 })
594 ];
595
596 unitConfig.RequiresMountsFor = "${cfg.dataDir}";
597 };
598
599 };
600
601 meta.doc = ./postgresql.md;
602 meta.maintainers = with lib.maintainers; [ thoughtpolice danbst ];
603}