1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 inherit (lib)
10 any
11 attrValues
12 concatMapStrings
13 concatStringsSep
14 const
15 elem
16 escapeShellArgs
17 filter
18 filterAttrs
19 getAttr
20 getName
21 hasPrefix
22 isString
23 literalExpression
24 mapAttrs
25 mapAttrsToList
26 mkAfter
27 mkBefore
28 mkDefault
29 mkEnableOption
30 mkIf
31 mkMerge
32 mkOption
33 mkPackageOption
34 mkRemovedOptionModule
35 mkRenamedOptionModule
36 optionalString
37 pipe
38 sortProperties
39 types
40 versionAtLeast
41 warn
42 ;
43
44 cfg = config.services.postgresql;
45
46 toStr =
47 value:
48 if true == value then
49 "yes"
50 else if false == value then
51 "no"
52 else if isString value then
53 "'${lib.replaceStrings [ "'" ] [ "''" ] value}'"
54 else
55 builtins.toString value;
56
57 # The main PostgreSQL configuration file.
58 configFile = pkgs.writeTextDir "postgresql.conf" (
59 concatStringsSep "\n" (
60 mapAttrsToList (n: v: "${n} = ${toStr v}") (filterAttrs (const (x: x != null)) cfg.settings)
61 )
62 );
63
64 configFileCheck = pkgs.runCommand "postgresql-configfile-check" { } ''
65 ${cfg.finalPackage}/bin/postgres -D${configFile} -C config_file >/dev/null
66 touch $out
67 '';
68
69 groupAccessAvailable = versionAtLeast cfg.finalPackage.version "11.0";
70
71 extensionNames = map getName cfg.finalPackage.installedExtensions;
72 extensionInstalled = extension: elem extension extensionNames;
73in
74
75{
76 imports = [
77 (mkRemovedOptionModule [
78 "services"
79 "postgresql"
80 "extraConfig"
81 ] "Use services.postgresql.settings instead.")
82
83 (mkRemovedOptionModule [
84 "services"
85 "postgresql"
86 "recoveryConfig"
87 ] "PostgreSQL v12+ doesn't support recovery.conf.")
88
89 (mkRenamedOptionModule
90 [ "services" "postgresql" "logLinePrefix" ]
91 [ "services" "postgresql" "settings" "log_line_prefix" ]
92 )
93 (mkRenamedOptionModule
94 [ "services" "postgresql" "port" ]
95 [ "services" "postgresql" "settings" "port" ]
96 )
97 (mkRenamedOptionModule
98 [ "services" "postgresql" "extraPlugins" ]
99 [ "services" "postgresql" "extensions" ]
100 )
101 ];
102
103 ###### interface
104
105 options = {
106
107 services.postgresql = {
108
109 enable = mkEnableOption "PostgreSQL Server";
110
111 enableJIT = mkEnableOption "JIT support";
112
113 package = mkOption {
114 type = types.package;
115 example = literalExpression "pkgs.postgresql_15";
116 defaultText = literalExpression ''
117 if versionAtLeast config.system.stateVersion "25.11" then
118 pkgs.postgresql_17
119 else if versionAtLeast config.system.stateVersion "24.11" then
120 pkgs.postgresql_16
121 else if versionAtLeast config.system.stateVersion "23.11" then
122 pkgs.postgresql_15
123 else if versionAtLeast config.system.stateVersion "22.05" then
124 pkgs.postgresql_14
125 else
126 pkgs.postgresql_13
127 '';
128 description = ''
129 The package being used by postgresql.
130 '';
131 };
132
133 finalPackage = mkOption {
134 type = types.package;
135 readOnly = true;
136 default =
137 let
138 # ensure that
139 # services.postgresql = {
140 # enableJIT = true;
141 # package = pkgs.postgresql_<major>;
142 # };
143 # works.
144 withJit = if cfg.enableJIT then cfg.package.withJIT else cfg.package.withoutJIT;
145 withJitAndPackages = if cfg.extensions == [ ] then withJit else withJit.withPackages cfg.extensions;
146 in
147 withJitAndPackages;
148 defaultText = "with config.services.postgresql; package.withPackages extensions";
149 description = ''
150 The postgresql package that will effectively be used in the system.
151 It consists of the base package with plugins applied to it.
152 '';
153 };
154
155 systemCallFilter = mkOption {
156 type = types.attrsOf (
157 types.coercedTo types.bool (enable: { inherit enable; }) (
158 types.submodule (
159 { name, ... }:
160 {
161 options = {
162 enable = mkEnableOption "${name} in postgresql's syscall filter";
163 priority = mkOption {
164 default =
165 if hasPrefix "@" name then
166 500
167 else if hasPrefix "~@" name then
168 1000
169 else
170 1500;
171 defaultText = literalExpression ''
172 if hasPrefix "@" name then 500 else if hasPrefix "~@" name then 1000 else 1500
173 '';
174 type = types.int;
175 description = ''
176 Set the priority of the system call filter setting. Later declarations
177 override earlier ones, e.g.
178
179 ```ini
180 [Service]
181 SystemCallFilter=~read write
182 SystemCallFilter=write
183 ```
184
185 results in a service where _only_ `read` is not allowed.
186
187 The ordering in the unit file is controlled by this option: the higher
188 the number, the later it will be added to the filterset.
189
190 By default, depending on the prefix a priority is assigned: usually, call-groups
191 (starting with `@`) are used to allow/deny a larger set of syscalls and later
192 on single syscalls are configured for exceptions. Hence, syscall groups
193 and negative groups are placed before individual syscalls by default.
194 '';
195 };
196 };
197 }
198 )
199 )
200 );
201 defaultText = literalExpression ''
202 {
203 "@system-service" = true;
204 "~@privileged" = true;
205 "~@resources" = true;
206 }
207 '';
208 description = ''
209 Configures the syscall filter for `postgresql.service`. The keys are
210 declarations for `SystemCallFilter` as described in {manpage}`systemd.exec(5)`.
211
212 The value is a boolean: `true` adds the attribute name to the syscall filter-set,
213 `false` doesn't. This is done to allow downstream configurations to turn off
214 restrictions made here. E.g. with
215
216 ```nix
217 {
218 services.postgresql.systemCallFilter."~@resources" = false;
219 }
220 ```
221
222 it's possible to remove the restriction on `@resources` (keep in mind that
223 `@system-service` implies `@resources`).
224
225 As described in the section for [](#opt-services.postgresql.systemCallFilter._name_.priority),
226 the ordering matters. Hence, it's also possible to specify customizations with
227
228 ```nix
229 {
230 services.postgresql.systemCallFilter = {
231 "foobar" = { enable = true; priority = 23; };
232 };
233 }
234 ```
235
236 [](#opt-services.postgresql.systemCallFilter._name_.enable) is the flag whether
237 or not it will be added to the `SystemCallFilter` of `postgresql.service`.
238
239 Settings with a higher priority are added after filter settings with a lower
240 priority. Hence, syscall groups with a higher priority can discard declarations
241 with a lower priority.
242
243 By default, syscall groups (i.e. attribute names starting with `@`) are added
244 _before_ negated groups (i.e. `~@` as prefix) _before_ syscall names
245 and negations.
246 '';
247 };
248
249 checkConfig = mkOption {
250 type = types.bool;
251 default = true;
252 description = "Check the syntax of the configuration file at compile time";
253 };
254
255 dataDir = mkOption {
256 type = types.path;
257 defaultText = literalExpression ''"/var/lib/postgresql/''${config.services.postgresql.package.psqlSchema}"'';
258 example = "/var/lib/postgresql/15";
259 description = ''
260 The data directory for PostgreSQL. If left as the default value
261 this directory will automatically be created before the PostgreSQL server starts, otherwise
262 the sysadmin is responsible for ensuring the directory exists with appropriate ownership
263 and permissions.
264 '';
265 };
266
267 authentication = mkOption {
268 type = types.lines;
269 default = "";
270 description = ''
271 Defines how users authenticate themselves to the server. See the
272 [PostgreSQL documentation for pg_hba.conf](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html)
273 for details on the expected format of this option. By default,
274 peer based authentication will be used for users connecting
275 via the Unix socket, and md5 password authentication will be
276 used for users connecting via TCP. Any added rules will be
277 inserted above the default rules. If you'd like to replace the
278 default rules entirely, you can use `lib.mkForce` in your
279 module.
280 '';
281 };
282
283 identMap = mkOption {
284 type = types.lines;
285 default = "";
286 example = ''
287 map-name-0 system-username-0 database-username-0
288 map-name-1 system-username-1 database-username-1
289 '';
290 description = ''
291 Defines the mapping from system users to database users.
292
293 See the [auth doc](https://postgresql.org/docs/current/auth-username-maps.html).
294
295 There is a default map "postgres" which is used for local peer authentication
296 as the postgres superuser role.
297 For example, to allow the root user to login as the postgres superuser, add:
298
299 ```
300 postgres root postgres
301 ```
302 '';
303 };
304
305 initdbArgs = mkOption {
306 type = with types; listOf str;
307 default = [ ];
308 example = [
309 "--data-checksums"
310 "--allow-group-access"
311 ];
312 description = ''
313 Additional arguments passed to `initdb` during data dir
314 initialisation.
315 '';
316 };
317
318 initialScript = mkOption {
319 type = types.nullOr types.path;
320 default = null;
321 example = literalExpression ''
322 pkgs.writeText "init-sql-script" '''
323 alter user postgres with password 'myPassword';
324 ''';'';
325
326 description = ''
327 A file containing SQL statements to execute on first startup.
328 '';
329 };
330
331 ensureDatabases = mkOption {
332 type = types.listOf types.str;
333 default = [ ];
334 description = ''
335 Ensures that the specified databases exist.
336 This option will never delete existing databases, especially not when the value of this
337 option is changed. This means that databases created once through this option or
338 otherwise have to be removed manually.
339 '';
340 example = [
341 "gitea"
342 "nextcloud"
343 ];
344 };
345
346 ensureUsers = mkOption {
347 type = types.listOf (
348 types.submodule {
349 options = {
350 name = mkOption {
351 type = types.str;
352 description = ''
353 Name of the user to ensure.
354 '';
355 };
356
357 ensureDBOwnership = mkOption {
358 type = types.bool;
359 default = false;
360 description = ''
361 Grants the user ownership to a database with the same name.
362 This database must be defined manually in
363 [](#opt-services.postgresql.ensureDatabases).
364 '';
365 };
366
367 ensureClauses = mkOption {
368 description = ''
369 An attrset of clauses to grant to the user. Under the hood this uses the
370 [ALTER USER syntax](https://www.postgresql.org/docs/current/sql-alteruser.html) for each attrName where
371 the attrValue is true in the attrSet:
372 `ALTER USER user.name WITH attrName`
373 '';
374 example = literalExpression ''
375 {
376 superuser = true;
377 createrole = true;
378 createdb = true;
379 }
380 '';
381 default = { };
382 defaultText = lib.literalMD ''
383 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.
384 '';
385 type = types.submodule {
386 options =
387 let
388 defaultText = lib.literalMD ''
389 `null`: do not set. For newly created roles, use PostgreSQL's default. For existing roles, do not touch this clause.
390 '';
391 in
392 {
393 superuser = mkOption {
394 type = types.nullOr types.bool;
395 description = ''
396 Grants the user, created by the ensureUser attr, superuser permissions. From the postgres docs:
397
398 A database superuser bypasses all permission checks,
399 except the right to log in. This is a dangerous privilege
400 and should not be used carelessly; it is best to do most
401 of your work as a role that is not a superuser. To create
402 a new database superuser, use CREATE ROLE name SUPERUSER.
403 You must do this as a role that is already a superuser.
404
405 More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
406 '';
407 default = null;
408 inherit defaultText;
409 };
410 createrole = mkOption {
411 type = types.nullOr types.bool;
412 description = ''
413 Grants the user, created by the ensureUser attr, createrole permissions. From the postgres docs:
414
415 A role must be explicitly given permission to create more
416 roles (except for superusers, since those bypass all
417 permission checks). To create such a role, use CREATE
418 ROLE name CREATEROLE. A role with CREATEROLE privilege
419 can alter and drop other roles, too, as well as grant or
420 revoke membership in them. However, to create, alter,
421 drop, or change membership of a superuser role, superuser
422 status is required; CREATEROLE is insufficient for that.
423
424 More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
425 '';
426 default = null;
427 inherit defaultText;
428 };
429 createdb = mkOption {
430 type = types.nullOr types.bool;
431 description = ''
432 Grants the user, created by the ensureUser attr, createdb permissions. From the postgres docs:
433
434 A role must be explicitly given permission to create
435 databases (except for superusers, since those bypass all
436 permission checks). To create such a role, use CREATE
437 ROLE name CREATEDB.
438
439 More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
440 '';
441 default = null;
442 inherit defaultText;
443 };
444 "inherit" = mkOption {
445 type = types.nullOr types.bool;
446 description = ''
447 Grants the user created inherit permissions. From the postgres docs:
448
449 A role is given permission to inherit the privileges of
450 roles it is a member of, by default. However, to create a
451 role without the permission, use CREATE ROLE name
452 NOINHERIT.
453
454 More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
455 '';
456 default = null;
457 inherit defaultText;
458 };
459 login = mkOption {
460 type = types.nullOr types.bool;
461 description = ''
462 Grants the user, created by the ensureUser attr, login permissions. From the postgres docs:
463
464 Only roles that have the LOGIN attribute can be used as
465 the initial role name for a database connection. A role
466 with the LOGIN attribute can be considered the same as a
467 “database user”. To create a role with login privilege,
468 use either:
469
470 CREATE ROLE name LOGIN; CREATE USER name;
471
472 (CREATE USER is equivalent to CREATE ROLE except that
473 CREATE USER includes LOGIN by default, while CREATE ROLE
474 does not.)
475
476 More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
477 '';
478 default = null;
479 inherit defaultText;
480 };
481 replication = mkOption {
482 type = types.nullOr types.bool;
483 description = ''
484 Grants the user, created by the ensureUser attr, replication permissions. From the postgres docs:
485
486 A role must explicitly be given permission to initiate
487 streaming replication (except for superusers, since those
488 bypass all permission checks). A role used for streaming
489 replication must have LOGIN permission as well. To create
490 such a role, use CREATE ROLE name REPLICATION LOGIN.
491
492 More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
493 '';
494 default = null;
495 inherit defaultText;
496 };
497 bypassrls = mkOption {
498 type = types.nullOr types.bool;
499 description = ''
500 Grants the user, created by the ensureUser attr, replication permissions. From the postgres docs:
501
502 A role must be explicitly given permission to bypass
503 every row-level security (RLS) policy (except for
504 superusers, since those bypass all permission checks). To
505 create such a role, use CREATE ROLE name BYPASSRLS as a
506 superuser.
507
508 More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
509 '';
510 default = null;
511 inherit defaultText;
512 };
513 };
514 };
515 };
516 };
517 }
518 );
519 default = [ ];
520 description = ''
521 Ensures that the specified users exist.
522 The PostgreSQL users will be identified using peer authentication. This authenticates the Unix user with the
523 same name only, and that without the need for a password.
524 This option will never delete existing users or remove DB ownership of databases
525 once granted with `ensureDBOwnership = true;`. This means that this must be
526 cleaned up manually when changing after changing the config in here.
527 '';
528 example = literalExpression ''
529 [
530 {
531 name = "nextcloud";
532 }
533 {
534 name = "superuser";
535 ensureDBOwnership = true;
536 }
537 ]
538 '';
539 };
540
541 enableTCPIP = mkOption {
542 type = types.bool;
543 default = false;
544 description = ''
545 Whether PostgreSQL should listen on all network interfaces.
546 If disabled, the database can only be accessed via its Unix
547 domain socket or via TCP connections to localhost.
548 '';
549 };
550
551 extensions = mkOption {
552 type = with types; coercedTo (listOf path) (path: _ignorePg: path) (functionTo (listOf path));
553 default = _: [ ];
554 example = literalExpression "ps: with ps; [ postgis pg_repack ]";
555 description = ''
556 List of PostgreSQL extensions to install.
557 '';
558 };
559
560 settings = mkOption {
561 type =
562 with types;
563 submodule {
564 freeformType = attrsOf (oneOf [
565 bool
566 float
567 int
568 str
569 ]);
570 options = {
571 shared_preload_libraries = mkOption {
572 type = nullOr (coercedTo (listOf str) (concatStringsSep ",") commas);
573 default = null;
574 example = literalExpression ''[ "auto_explain" "anon" ]'';
575 description = ''
576 List of libraries to be preloaded.
577 '';
578 };
579
580 log_line_prefix = mkOption {
581 type = types.str;
582 default = "[%p] ";
583 example = "%m [%p] ";
584 description = ''
585 A printf-style string that is output at the beginning of each log line.
586 Upstream default is `'%m [%p] '`, i.e. it includes the timestamp. We do
587 not include the timestamp, because journal has it anyway.
588 '';
589 };
590
591 port = mkOption {
592 type = types.port;
593 default = 5432;
594 description = ''
595 The port on which PostgreSQL listens.
596 '';
597 };
598 };
599 };
600 default = { };
601 description = ''
602 PostgreSQL configuration. Refer to
603 <https://www.postgresql.org/docs/current/config-setting.html#CONFIG-SETTING-CONFIGURATION-FILE>
604 for an overview of `postgresql.conf`.
605
606 ::: {.note}
607 String values will automatically be enclosed in single quotes. Single quotes will be
608 escaped with two single quotes as described by the upstream documentation linked above.
609 :::
610 '';
611 example = literalExpression ''
612 {
613 log_connections = true;
614 log_statement = "all";
615 logging_collector = true;
616 log_disconnections = true;
617 log_destination = lib.mkForce "syslog";
618 }
619 '';
620 };
621
622 superUser = mkOption {
623 type = types.str;
624 default = "postgres";
625 internal = true;
626 readOnly = true;
627 description = ''
628 PostgreSQL superuser account to use for various operations. Internal since changing
629 this value would lead to breakage while setting up databases.
630 '';
631 };
632 };
633
634 };
635
636 ###### implementation
637
638 config = mkIf cfg.enable {
639
640 warnings = (
641 let
642 unstableState =
643 if lib.hasInfix "beta" cfg.package.version then
644 "in beta"
645 else if lib.hasInfix "rc" cfg.package.version then
646 "a release candidate"
647 else
648 null;
649 in
650 lib.optional (unstableState != null)
651 "PostgreSQL ${lib.versions.major cfg.package.version} is currently ${unstableState}, and is not advised for use in production environments."
652 );
653
654 assertions = map (
655 { name, ensureDBOwnership, ... }:
656 {
657 assertion = ensureDBOwnership -> elem name cfg.ensureDatabases;
658 message = ''
659 For each database user defined with `services.postgresql.ensureUsers` and
660 `ensureDBOwnership = true;`, a database with the same name must be defined
661 in `services.postgresql.ensureDatabases`.
662
663 Offender: ${name} has not been found among databases.
664 '';
665 }
666 ) cfg.ensureUsers;
667
668 services.postgresql.settings = {
669 hba_file = "${pkgs.writeText "pg_hba.conf" cfg.authentication}";
670 ident_file = "${pkgs.writeText "pg_ident.conf" cfg.identMap}";
671 log_destination = "stderr";
672 listen_addresses = if cfg.enableTCPIP then "*" else "localhost";
673 jit = mkDefault (if cfg.enableJIT then "on" else "off");
674 };
675
676 services.postgresql.package =
677 let
678 mkThrow = ver: throw "postgresql_${ver} was removed, please upgrade your postgresql version.";
679 mkWarn =
680 ver:
681 warn ''
682 The postgresql package is not pinned and selected automatically by
683 `system.stateVersion`. Right now this is `pkgs.postgresql_${ver}`, the
684 oldest postgresql version available and thus the next that will be
685 removed when EOL on the next stable cycle.
686
687 See also https://endoflife.date/postgresql
688 '';
689 base =
690 # XXX Don't forget to keep `defaultText` of `services.postgresql.package` up to date!
691 if versionAtLeast config.system.stateVersion "25.11" then
692 pkgs.postgresql_17
693 else if versionAtLeast config.system.stateVersion "24.11" then
694 pkgs.postgresql_16
695 else if versionAtLeast config.system.stateVersion "23.11" then
696 pkgs.postgresql_15
697 else if versionAtLeast config.system.stateVersion "22.05" then
698 pkgs.postgresql_14
699 else if versionAtLeast config.system.stateVersion "21.11" then
700 mkWarn "13" pkgs.postgresql_13
701 else if versionAtLeast config.system.stateVersion "20.03" then
702 mkThrow "11"
703 else if versionAtLeast config.system.stateVersion "17.09" then
704 mkThrow "9_6"
705 else
706 mkThrow "9_5";
707 in
708 # Note: when changing the default, make it conditional on
709 # ‘system.stateVersion’ to maintain compatibility with existing
710 # systems!
711 mkDefault (if cfg.enableJIT then base.withJIT else base);
712
713 services.postgresql.dataDir = mkDefault "/var/lib/postgresql/${cfg.package.psqlSchema}";
714
715 services.postgresql.authentication = mkMerge [
716 (mkBefore "# Generated file; do not edit!")
717 (mkAfter ''
718 # default value of services.postgresql.authentication
719 local all postgres peer map=postgres
720 local all all peer
721 host all all 127.0.0.1/32 md5
722 host all all ::1/128 md5
723 '')
724 ];
725
726 # The default allows to login with the same database username as the current system user.
727 # This is the default for peer authentication without a map, but needs to be made explicit
728 # once a map is used.
729 services.postgresql.identMap = mkAfter ''
730 postgres postgres postgres
731 '';
732
733 services.postgresql.systemCallFilter = mkMerge [
734 (mapAttrs (const mkDefault) {
735 "@system-service" = true;
736 "~@privileged" = true;
737 "~@resources" = true;
738 })
739 (mkIf (any extensionInstalled [ "plv8" ]) {
740 "@pkey" = true;
741 })
742 (mkIf (any extensionInstalled [ "citus" ]) {
743 "getpriority" = true;
744 "setpriority" = true;
745 })
746 ];
747
748 users.users.postgres = {
749 name = "postgres";
750 uid = config.ids.uids.postgres;
751 group = "postgres";
752 description = "PostgreSQL server user";
753 home = "${cfg.dataDir}";
754 useDefaultShell = true;
755 };
756
757 users.groups.postgres.gid = config.ids.gids.postgres;
758
759 environment.systemPackages = [ cfg.finalPackage ];
760
761 environment.pathsToLink = [
762 "/share/postgresql"
763 ];
764
765 system.checks = lib.optional (
766 cfg.checkConfig && pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform
767 ) configFileCheck;
768
769 systemd.targets.postgresql = {
770 description = "PostgreSQL";
771 wantedBy = [ "multi-user.target" ];
772 requires = [
773 "postgresql.service"
774 "postgresql-setup.service"
775 ];
776 };
777
778 systemd.services.postgresql = {
779 description = "PostgreSQL Server";
780
781 after = [ "network.target" ];
782
783 # To trigger the .target also on "systemctl start postgresql" as well as on
784 # restarts & stops.
785 # Please note that postgresql.service & postgresql.target binding to
786 # each other makes the Restart=always rule racy and results
787 # in sometimes the service not being restarted.
788 wants = [ "postgresql.target" ];
789 partOf = [ "postgresql.target" ];
790
791 environment.PGDATA = cfg.dataDir;
792
793 path = [ cfg.finalPackage ];
794
795 preStart = ''
796 if ! test -e ${cfg.dataDir}/PG_VERSION; then
797 # Cleanup the data directory.
798 rm -f ${cfg.dataDir}/*.conf
799
800 # Initialise the database.
801 initdb -U ${cfg.superUser} ${escapeShellArgs cfg.initdbArgs}
802
803 # See postStart!
804 touch "${cfg.dataDir}/.first_startup"
805 fi
806
807 ln -sfn "${configFile}/postgresql.conf" "${cfg.dataDir}/postgresql.conf"
808 '';
809
810 serviceConfig = mkMerge [
811 {
812 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
813 User = "postgres";
814 Group = "postgres";
815 RuntimeDirectory = "postgresql";
816 Type = if versionAtLeast cfg.package.version "9.6" then "notify" else "simple";
817
818 # Shut down Postgres using SIGINT ("Fast Shutdown mode"). See
819 # https://www.postgresql.org/docs/current/server-shutdown.html
820 KillSignal = "SIGINT";
821 KillMode = "mixed";
822
823 # Give Postgres a decent amount of time to clean up after
824 # receiving systemd's SIGINT.
825 TimeoutSec = 120;
826
827 ExecStart = "${cfg.finalPackage}/bin/postgres";
828
829 Restart = "always";
830
831 # Hardening
832 CapabilityBoundingSet = [ "" ];
833 DevicePolicy = "closed";
834 PrivateTmp = true;
835 ProtectHome = true;
836 ProtectSystem = "strict";
837 MemoryDenyWriteExecute = lib.mkDefault (
838 cfg.settings.jit == "off" && (!any extensionInstalled [ "plv8" ])
839 );
840 NoNewPrivileges = true;
841 LockPersonality = true;
842 PrivateDevices = true;
843 PrivateMounts = true;
844 ProcSubset = "pid";
845 ProtectClock = true;
846 ProtectControlGroups = true;
847 ProtectHostname = true;
848 ProtectKernelLogs = true;
849 ProtectKernelModules = true;
850 ProtectKernelTunables = true;
851 ProtectProc = "invisible";
852 RemoveIPC = true;
853 RestrictAddressFamilies = [
854 "AF_INET"
855 "AF_INET6"
856 "AF_NETLINK" # used for network interface enumeration
857 "AF_UNIX"
858 ];
859 RestrictNamespaces = true;
860 RestrictRealtime = true;
861 RestrictSUIDSGID = true;
862 SystemCallArchitectures = "native";
863 SystemCallFilter = pipe cfg.systemCallFilter [
864 (mapAttrsToList (name: v: v // { inherit name; }))
865 (filter (getAttr "enable"))
866 sortProperties
867 (map (getAttr "name"))
868 ];
869 UMask = if groupAccessAvailable then "0027" else "0077";
870 }
871 (mkIf (cfg.dataDir != "/var/lib/postgresql/${cfg.package.psqlSchema}") {
872 # The user provides their own data directory
873 ReadWritePaths = [ cfg.dataDir ];
874 })
875 (mkIf (cfg.dataDir == "/var/lib/postgresql/${cfg.package.psqlSchema}") {
876 # Provision the default data directory
877 StateDirectory = "postgresql postgresql/${cfg.package.psqlSchema}";
878 StateDirectoryMode = if groupAccessAvailable then "0750" else "0700";
879 })
880 ];
881
882 unitConfig =
883 let
884 inherit (config.systemd.services.postgresql.serviceConfig) TimeoutSec;
885 maxTries = 5;
886 bufferSec = 5;
887 in
888 {
889 RequiresMountsFor = "${cfg.dataDir}";
890
891 # The max. time needed to perform `maxTries` start attempts of systemd
892 # plus a bit of buffer time (bufferSec) on top.
893 StartLimitIntervalSec = TimeoutSec * maxTries + bufferSec;
894 StartLimitBurst = maxTries;
895 };
896 };
897
898 systemd.services.postgresql-setup = {
899 description = "PostgreSQL Setup Scripts";
900
901 requires = [ "postgresql.service" ];
902 after = [ "postgresql.service" ];
903
904 serviceConfig = {
905 User = "postgres";
906 Group = "postgres";
907 Type = "oneshot";
908 RemainAfterExit = true;
909 };
910
911 path = [ cfg.finalPackage ];
912 environment.PGPORT = builtins.toString cfg.settings.port;
913
914 # Wait for PostgreSQL to be ready to accept connections.
915 script = ''
916 check-connection() {
917 psql -d postgres -v ON_ERROR_STOP=1 <<-' EOF'
918 SELECT pg_is_in_recovery() \gset
919 \if :pg_is_in_recovery
920 \i still-recovering
921 \endif
922 EOF
923 }
924 while ! check-connection 2> /dev/null; do
925 if ! systemctl is-active --quiet postgresql.service; then exit 1; fi
926 sleep 0.1
927 done
928
929 if test -e "${cfg.dataDir}/.first_startup"; then
930 ${optionalString (cfg.initialScript != null) ''
931 psql -f "${cfg.initialScript}" -d postgres
932 ''}
933 rm -f "${cfg.dataDir}/.first_startup"
934 fi
935 ''
936 + optionalString (cfg.ensureDatabases != [ ]) ''
937 ${concatMapStrings (database: ''
938 psql -tAc "SELECT 1 FROM pg_database WHERE datname = '${database}'" | grep -q 1 || psql -tAc 'CREATE DATABASE "${database}"'
939 '') cfg.ensureDatabases}
940 ''
941 + ''
942 ${concatMapStrings (
943 user:
944 let
945 dbOwnershipStmt = optionalString user.ensureDBOwnership ''psql -tAc 'ALTER DATABASE "${user.name}" OWNER TO "${user.name}";' '';
946
947 filteredClauses = filterAttrs (name: value: value != null) user.ensureClauses;
948
949 clauseSqlStatements = attrValues (mapAttrs (n: v: if v then n else "no${n}") filteredClauses);
950
951 userClauses = ''psql -tAc 'ALTER ROLE "${user.name}" ${concatStringsSep " " clauseSqlStatements}' '';
952 in
953 ''
954 psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${user.name}'" | grep -q 1 || psql -tAc 'CREATE USER "${user.name}"'
955 ${userClauses}
956
957 ${dbOwnershipStmt}
958 ''
959 ) cfg.ensureUsers}
960 '';
961 };
962 };
963
964 meta.doc = ./postgresql.md;
965 meta.maintainers = pkgs.postgresql.meta.maintainers;
966}