1 { config, lib, pkgs, ... }:
34 cfg = config.services.postgresql;
37 # services.postgresql = {
39 # package = pkgs.postgresql_<major>;
42 basePackage = if cfg.enableJIT
43 then cfg.package.withJIT
44 else cfg.package.withoutJIT;
46 postgresql = if cfg.extensions == []
48 else basePackage.withPackages cfg.extensions;
51 if true == value then "yes"
52 else if false == value then "no"
53 else if isString value then "'${lib.replaceStrings ["'"] ["''"] value}'"
54 else builtins.toString value;
56 # The main PostgreSQL configuration file.
57 configFile = pkgs.writeTextDir "postgresql.conf" (concatStringsSep "\n" (mapAttrsToList (n: v: "${n} = ${toStr v}") (filterAttrs (const (x: x != null)) cfg.settings)));
59 configFileCheck = pkgs.runCommand "postgresql-configfile-check" {} ''
60 ${cfg.package}/bin/postgres -D${configFile} -C config_file >/dev/null
64 groupAccessAvailable = versionAtLeast postgresql.version "11.0";
66 extensionNames = map getName postgresql.installedExtensions;
67 extensionInstalled = extension: elem extension extensionNames;
72 (mkRemovedOptionModule [ "services" "postgresql" "extraConfig" ] "Use services.postgresql.settings instead.")
74 (mkRenamedOptionModule [ "services" "postgresql" "logLinePrefix" ] [ "services" "postgresql" "settings" "log_line_prefix" ])
75 (mkRenamedOptionModule [ "services" "postgresql" "port" ] [ "services" "postgresql" "settings" "port" ])
76 (mkRenamedOptionModule [ "services" "postgresql" "extraPlugins" ] [ "services" "postgresql" "extensions" ])
83 services.postgresql = {
85 enable = mkEnableOption "PostgreSQL Server";
87 enableJIT = mkEnableOption "JIT support";
89 package = mkPackageOption pkgs "postgresql" {
90 example = "postgresql_15";
93 checkConfig = mkOption {
96 description = "Check the syntax of the configuration file at compile time";
101 defaultText = literalExpression ''"/var/lib/postgresql/''${config.services.postgresql.package.psqlSchema}"'';
102 example = "/var/lib/postgresql/15";
104 The data directory for PostgreSQL. If left as the default value
105 this directory will automatically be created before the PostgreSQL server starts, otherwise
106 the sysadmin is responsible for ensuring the directory exists with appropriate ownership
111 authentication = mkOption {
115 Defines how users authenticate themselves to the server. See the
116 [PostgreSQL documentation for pg_hba.conf](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html)
117 for details on the expected format of this option. By default,
118 peer based authentication will be used for users connecting
119 via the Unix socket, and md5 password authentication will be
120 used for users connecting via TCP. Any added rules will be
121 inserted above the default rules. If you'd like to replace the
122 default rules entirely, you can use `lib.mkForce` in your
127 identMap = mkOption {
131 map-name-0 system-username-0 database-username-0
132 map-name-1 system-username-1 database-username-1
135 Defines the mapping from system users to database users.
137 See the [auth doc](https://postgresql.org/docs/current/auth-username-maps.html).
141 initdbArgs = mkOption {
142 type = with types; listOf str;
144 example = [ "--data-checksums" "--allow-group-access" ];
146 Additional arguments passed to `initdb` during data dir
151 initialScript = mkOption {
152 type = types.nullOr types.path;
154 example = literalExpression ''
155 pkgs.writeText "init-sql-script" '''
156 alter user postgres with password 'myPassword';
160 A file containing SQL statements to execute on first startup.
164 ensureDatabases = mkOption {
165 type = types.listOf types.str;
168 Ensures that the specified databases exist.
169 This option will never delete existing databases, especially not when the value of this
170 option is changed. This means that databases created once through this option or
171 otherwise have to be removed manually.
179 ensureUsers = mkOption {
180 type = types.listOf (types.submodule {
185 Name of the user to ensure.
189 ensureDBOwnership = mkOption {
193 Grants the user ownership to a database with the same name.
194 This database must be defined manually in
195 [](#opt-services.postgresql.ensureDatabases).
199 ensureClauses = mkOption {
201 An attrset of clauses to grant to the user. Under the hood this uses the
202 [ALTER USER syntax](https://www.postgresql.org/docs/current/sql-alteruser.html) for each attrName where
203 the attrValue is true in the attrSet:
204 `ALTER USER user.name WITH attrName`
206 example = literalExpression ''
214 defaultText = lib.literalMD ''
215 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.
217 type = types.submodule {
219 defaultText = lib.literalMD ''
220 `null`: do not set. For newly created roles, use PostgreSQL's default. For existing roles, do not touch this clause.
223 superuser = mkOption {
224 type = types.nullOr types.bool;
226 Grants the user, created by the ensureUser attr, superuser permissions. From the postgres docs:
228 A database superuser bypasses all permission checks,
229 except the right to log in. This is a dangerous privilege
230 and should not be used carelessly; it is best to do most
231 of your work as a role that is not a superuser. To create
232 a new database superuser, use CREATE ROLE name SUPERUSER.
233 You must do this as a role that is already a superuser.
235 More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
240 createrole = mkOption {
241 type = types.nullOr types.bool;
243 Grants the user, created by the ensureUser attr, createrole permissions. From the postgres docs:
245 A role must be explicitly given permission to create more
246 roles (except for superusers, since those bypass all
247 permission checks). To create such a role, use CREATE
248 ROLE name CREATEROLE. A role with CREATEROLE privilege
249 can alter and drop other roles, too, as well as grant or
250 revoke membership in them. However, to create, alter,
251 drop, or change membership of a superuser role, superuser
252 status is required; CREATEROLE is insufficient for that.
254 More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
259 createdb = mkOption {
260 type = types.nullOr types.bool;
262 Grants the user, created by the ensureUser attr, createdb permissions. From the postgres docs:
264 A role must be explicitly given permission to create
265 databases (except for superusers, since those bypass all
266 permission checks). To create such a role, use CREATE
269 More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
274 "inherit" = mkOption {
275 type = types.nullOr types.bool;
277 Grants the user created inherit permissions. From the postgres docs:
279 A role is given permission to inherit the privileges of
280 roles it is a member of, by default. However, to create a
281 role without the permission, use CREATE ROLE name
284 More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
290 type = types.nullOr types.bool;
292 Grants the user, created by the ensureUser attr, login permissions. From the postgres docs:
294 Only roles that have the LOGIN attribute can be used as
295 the initial role name for a database connection. A role
296 with the LOGIN attribute can be considered the same as a
297 “database user”. To create a role with login privilege,
300 CREATE ROLE name LOGIN; CREATE USER name;
302 (CREATE USER is equivalent to CREATE ROLE except that
303 CREATE USER includes LOGIN by default, while CREATE ROLE
306 More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
311 replication = mkOption {
312 type = types.nullOr types.bool;
314 Grants the user, created by the ensureUser attr, replication permissions. From the postgres docs:
316 A role must explicitly be given permission to initiate
317 streaming replication (except for superusers, since those
318 bypass all permission checks). A role used for streaming
319 replication must have LOGIN permission as well. To create
320 such a role, use CREATE ROLE name REPLICATION LOGIN.
322 More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
327 bypassrls = mkOption {
328 type = types.nullOr types.bool;
330 Grants the user, created by the ensureUser attr, replication permissions. From the postgres docs:
332 A role must be explicitly given permission to bypass
333 every row-level security (RLS) policy (except for
334 superusers, since those bypass all permission checks). To
335 create such a role, use CREATE ROLE name BYPASSRLS as a
338 More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
350 Ensures that the specified users exist.
351 The PostgreSQL users will be identified using peer authentication. This authenticates the Unix user with the
352 same name only, and that without the need for a password.
353 This option will never delete existing users or remove DB ownership of databases
354 once granted with `ensureDBOwnership = true;`. This means that this must be
355 cleaned up manually when changing after changing the config in here.
357 example = literalExpression ''
364 ensureDBOwnership = true;
370 enableTCPIP = mkOption {
374 Whether PostgreSQL should listen on all network interfaces.
375 If disabled, the database can only be accessed via its Unix
376 domain socket or via TCP connections to localhost.
380 extensions = mkOption {
381 type = with types; coercedTo (listOf path) (path: _ignorePg: path) (functionTo (listOf path));
383 example = literalExpression "ps: with ps; [ postgis pg_repack ]";
385 List of PostgreSQL extensions to install.
389 settings = mkOption {
390 type = with types; submodule {
391 freeformType = attrsOf (oneOf [ bool float int str ]);
393 shared_preload_libraries = mkOption {
394 type = nullOr (coercedTo (listOf str) (concatStringsSep ", ") str);
396 example = literalExpression ''[ "auto_explain" "anon" ]'';
398 List of libraries to be preloaded.
402 log_line_prefix = mkOption {
405 example = "%m [%p] ";
407 A printf-style string that is output at the beginning of each log line.
408 Upstream default is `'%m [%p] '`, i.e. it includes the timestamp. We do
409 not include the timestamp, because journal has it anyway.
417 The port on which PostgreSQL listens.
424 PostgreSQL configuration. Refer to
425 <https://www.postgresql.org/docs/current/config-setting.html#CONFIG-SETTING-CONFIGURATION-FILE>
426 for an overview of `postgresql.conf`.
429 String values will automatically be enclosed in single quotes. Single quotes will be
430 escaped with two single quotes as described by the upstream documentation linked above.
433 example = literalExpression ''
435 log_connections = true;
436 log_statement = "all";
437 logging_collector = true;
438 log_disconnections = true;
439 log_destination = lib.mkForce "syslog";
444 recoveryConfig = mkOption {
445 type = types.nullOr types.lines;
448 Contents of the {file}`recovery.conf` file.
452 superUser = mkOption {
454 default = "postgres";
458 PostgreSQL superuser account to use for various operations. Internal since changing
459 this value would lead to breakage while setting up databases.
467 ###### implementation
469 config = mkIf cfg.enable {
471 assertions = map ({ name, ensureDBOwnership, ... }: {
472 assertion = ensureDBOwnership -> elem name cfg.ensureDatabases;
474 For each database user defined with `services.postgresql.ensureUsers` and
475 `ensureDBOwnership = true;`, a database with the same name must be defined
476 in `services.postgresql.ensureDatabases`.
478 Offender: ${name} has not been found among databases.
482 services.postgresql.settings =
484 hba_file = "${pkgs.writeText "pg_hba.conf" cfg.authentication}";
485 ident_file = "${pkgs.writeText "pg_ident.conf" cfg.identMap}";
486 log_destination = "stderr";
487 listen_addresses = if cfg.enableTCPIP then "*" else "localhost";
488 jit = mkDefault (if cfg.enableJIT then "on" else "off");
491 services.postgresql.package = let
492 mkThrow = ver: throw "postgresql_${ver} was removed, please upgrade your postgresql version.";
493 mkWarn = ver: warn ''
494 The postgresql package is not pinned and selected automatically by
495 `system.stateVersion`. Right now this is `pkgs.postgresql_${ver}`, the
496 oldest postgresql version available and thus the next that will be
497 removed when EOL on the next stable cycle.
499 See also https://endoflife.date/postgresql
501 base = if versionAtLeast config.system.stateVersion "24.11" then pkgs.postgresql_16
502 else if versionAtLeast config.system.stateVersion "23.11" then pkgs.postgresql_15
503 else if versionAtLeast config.system.stateVersion "22.05" then pkgs.postgresql_14
504 else if versionAtLeast config.system.stateVersion "21.11" then mkWarn "13" pkgs.postgresql_13
505 else if versionAtLeast config.system.stateVersion "20.03" then mkThrow "11"
506 else if versionAtLeast config.system.stateVersion "17.09" then mkThrow "9_6"
509 # Note: when changing the default, make it conditional on
510 # ‘system.stateVersion’ to maintain compatibility with existing
512 mkDefault (if cfg.enableJIT then base.withJIT else base);
514 services.postgresql.dataDir = mkDefault "/var/lib/postgresql/${cfg.package.psqlSchema}";
516 services.postgresql.authentication = mkMerge [
517 (mkBefore "# Generated file; do not edit!")
520 # default value of services.postgresql.authentication
522 host all all 127.0.0.1/32 md5
523 host all all ::1/128 md5
527 users.users.postgres =
529 uid = config.ids.uids.postgres;
531 description = "PostgreSQL server user";
532 home = "${cfg.dataDir}";
533 useDefaultShell = true;
536 users.groups.postgres.gid = config.ids.gids.postgres;
538 environment.systemPackages = [ postgresql ];
540 environment.pathsToLink = [
544 system.checks = lib.optional (cfg.checkConfig && pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) configFileCheck;
546 systemd.services.postgresql =
547 { description = "PostgreSQL Server";
549 wantedBy = [ "multi-user.target" ];
550 after = [ "network.target" ];
552 environment.PGDATA = cfg.dataDir;
554 path = [ postgresql ];
558 if ! test -e ${cfg.dataDir}/PG_VERSION; then
559 # Cleanup the data directory.
560 rm -f ${cfg.dataDir}/*.conf
562 # Initialise the database.
563 initdb -U ${cfg.superUser} ${escapeShellArgs cfg.initdbArgs}
566 touch "${cfg.dataDir}/.first_startup"
569 ln -sfn "${configFile}/postgresql.conf" "${cfg.dataDir}/postgresql.conf"
570 ${optionalString (cfg.recoveryConfig != null) ''
571 ln -sfn "${pkgs.writeText "recovery.conf" cfg.recoveryConfig}" \
572 "${cfg.dataDir}/recovery.conf"
576 # Wait for PostgreSQL to be ready to accept connections.
579 PSQL="psql --port=${builtins.toString cfg.settings.port}"
581 while ! $PSQL -d postgres -c "" 2> /dev/null; do
582 if ! kill -0 "$MAINPID"; then exit 1; fi
586 if test -e "${cfg.dataDir}/.first_startup"; then
587 ${optionalString (cfg.initialScript != null) ''
588 $PSQL -f "${cfg.initialScript}" -d postgres
590 rm -f "${cfg.dataDir}/.first_startup"
592 '' + optionalString (cfg.ensureDatabases != []) ''
593 ${concatMapStrings (database: ''
594 $PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${database}'" | grep -q 1 || $PSQL -tAc 'CREATE DATABASE "${database}"'
595 '') cfg.ensureDatabases}
601 dbOwnershipStmt = optionalString
602 user.ensureDBOwnership
603 ''$PSQL -tAc 'ALTER DATABASE "${user.name}" OWNER TO "${user.name}";' '';
605 filteredClauses = filterAttrs (name: value: value != null) user.ensureClauses;
607 clauseSqlStatements = attrValues (mapAttrs (n: v: if v then n else "no${n}") filteredClauses);
609 userClauses = ''$PSQL -tAc 'ALTER ROLE "${user.name}" ${concatStringsSep " " clauseSqlStatements}' '';
611 $PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname='${user.name}'" | grep -q 1 || $PSQL -tAc 'CREATE USER "${user.name}"'
621 serviceConfig = mkMerge [
622 { ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
625 RuntimeDirectory = "postgresql";
626 Type = if versionAtLeast cfg.package.version "9.6"
630 # Shut down Postgres using SIGINT ("Fast Shutdown mode"). See
631 # https://www.postgresql.org/docs/current/server-shutdown.html
632 KillSignal = "SIGINT";
635 # Give Postgres a decent amount of time to clean up after
636 # receiving systemd's SIGINT.
639 ExecStart = "${postgresql}/bin/postgres";
642 CapabilityBoundingSet = [ "" ];
643 DevicePolicy = "closed";
646 ProtectSystem = "strict";
647 MemoryDenyWriteExecute = lib.mkDefault (cfg.settings.jit == "off" && (!any extensionInstalled [ "plv8" ]));
648 NoNewPrivileges = true;
649 LockPersonality = true;
650 PrivateDevices = true;
651 PrivateMounts = true;
654 ProtectControlGroups = true;
655 ProtectHostname = true;
656 ProtectKernelLogs = true;
657 ProtectKernelModules = true;
658 ProtectKernelTunables = true;
659 ProtectProc = "invisible";
661 RestrictAddressFamilies = [
664 "AF_NETLINK" # used for network interface enumeration
667 RestrictNamespaces = true;
668 RestrictRealtime = true;
669 RestrictSUIDSGID = true;
670 SystemCallArchitectures = "native";
674 "~@privileged @resources"
676 ++ lib.optionals (any extensionInstalled [ "plv8" ]) [ "@pkey" ];
677 UMask = if groupAccessAvailable then "0027" else "0077";
679 (mkIf (cfg.dataDir != "/var/lib/postgresql") {
680 ReadWritePaths = [ cfg.dataDir ];
682 (mkIf (cfg.dataDir == "/var/lib/postgresql/${cfg.package.psqlSchema}") {
683 StateDirectory = "postgresql postgresql/${cfg.package.psqlSchema}";
684 StateDirectoryMode = if groupAccessAvailable then "0750" else "0700";
688 unitConfig.RequiresMountsFor = "${cfg.dataDir}";
693 meta.doc = ./postgresql.md;
694 meta.maintainers = with lib.maintainers; [ thoughtpolice danbst ];