☰
Devenv 声明式配置 PostgreSQL 服务:services.postgres 模块配置与源码级解析
2026/9/29 5:41:45 网站建设 项目流程
  • 开发工具
  • CLI

【免费下载链接】devenv

Fast, Declarative, Reproducible, and Composable Developer Environments using Nix

项目地址:https://gitcode.com/gh_mirrors/de/devenv
点击查看免费下载

devenv 通过services.postgres模块提供开箱即用的 PostgreSQL 开发环境:一行enable = true即可启动数据库进程,并通过 Nix 声明式地完成建库、建用户、初始化 SQL、扩展安装与postgresql.conf调优。本文以 docs/src/content/docs/services/postgres.md 的选项文档为骨架,结合 src/modules/services/postgres.nix 的实现与仓库内真实测试用例,逐项讲解每个配置项的语义、默认值与运行机制,帮助你写出可复现、可迁移的 PostgreSQL 开发环境配置。

快速开始:从零启动一个 PostgreSQL

devenv 将服务抽象在processes之上:processes 提供运行任意命令的底层控制,而 services(如 PostgreSQL)为数据库这类既有软件提供预配置接口。services 与 processes 一样通过devenv up启动:

$ devenv up Starting processes ...

如果希望服务在后台运行,传入-d标志:

$ devenv up -d

一个最小的 PostgreSQL 配置如下(参考 docs/src/content/docs/services/index.md 中的官方示例):

{ pkgs, ... }: { services.postgres = { enable = true; package = pkgs.postgresql_15; initialDatabases = [{ name = "mydb"; }]; extensions = extensions: [ extensions.postgis extensions.timescaledb ]; settings.shared_preload_libraries = "timescaledb"; initialScript = "CREATE EXTENSION IF NOT EXISTS timescaledb;"; }; }

这份配置会:安装 PostgreSQL 15 与 postgis、timescaledb 两个扩展,在首次启动时创建名为mydb的数据库,加载timescaledb共享库并执行initialScript。服务状态持久化在$DEVENV_STATE下的目录中——当你调整了initialScript这类只在首次启动生效的选项后,需要删除服务对应的状态目录,改动才能在下次devenv up时生效。

核心开关与版本选择

services.postgres.enable

属性值
类型boolean
默认值false
示例true

启用 PostgreSQL 进程。注意 devenv 同时提供了一个重命名兼容机制:postgres.enable这一旧路径会被自动迁移到services.postgres.enable(见 src/modules/services/postgres.nix 中的mkRenamedOptionModule导入)。

services.postgres.package

属性值
类型package
默认值pkgs.postgresql
示例pkgs.postgresql_15

指定使用的 PostgreSQL 包,用于覆盖默认版本(例如锁定到postgresql_15或postgresql_16)。在模块内部,package的取值决定了 shell 中可用的二进制:

packages = [ (lib.getBin postgresPkg) startScript ];

这里有一个值得注意的实现细节:从 2026 年起的版本行为看,模块只把package中存放服务端与客户端二进制(postgres、psql、pg_ctl等)的binoutput 加入 shell,而不是整个包。这样做避免了把 PostgreSQL 构建期依赖(约 2.4 GB 的 LLVM、Perl、Python、Tcl)以及 libpq 头文件和libpq.pc污染 shell 的 include 与 pkg-config 路径。如果你的应用需要链接或加载 libpq 客户端库(例如 Ruby 的pggem、从源码构建的psycopg2或纯 Python 的psycopg),需要自行将pkgs.libpq加入packages;需要pg_config的构建可以使用pkgs.libpq.pg_config。

services.postgres.createDatabase

属性值
类型boolean
默认值true

在启动时创建一个与当前用户名同名的数据库。该选项仅在initialDatabases为空列表时生效。对应实现位于setupInitialDatabases的 else 分支:

psql --dbname postgres << EOF CREATE DATABASE "''${USER:-$(id -nu)}"; EOF

网络监听:listen_addresses 与 port

services.postgres.listen_addresses

属性值
类型string
默认值""
示例"127.0.0.1"

以逗号分隔的 TCP/IP 地址列表,指定服务端监听的网络接口。默认情况下服务只接受 Unix socket 连接,不会暴露任何 TCP 端口。该选项同时被解析用来设置PGHOST环境变量,支持的特殊值:

  • *:监听所有可用网络接口(对应实现会将其映射为127.0.0.1用于PGHOST)
  • 0.0.0.0:监听所有可用 IPv4 接口(映射为127.0.0.1)
  • :::监听所有可用 IPv6 接口(映射为::1)
  • localhost:仅监听回环接口
  • ""(空字符串):禁用 TCP/IP,仅监听 Unix socket(默认行为)

实现上,parseListenAddresses会把输入按逗号拆分、trim、将*/0.0.0.0/::转换为回环地址,再取第一个元素作为PGHOST;若listen_addresses为空,则PGHOST指向运行时 socket 目录$DEVENV_RUNTIME/postgres:

env.PGHOST = let parsedAddress = headWithDefault null (parseListenAddresses cfg.listen_addresses); host = if cfg.listen_addresses != "" then parsedAddress else runtimeDir; in lib.mkDefault host;

services.postgres.port

属性值
类型16 位无符号整数(0–65535)
默认值5432

TCP 监听端口。启用网络监听(listen_addresses != "")后,模块会通过processes.postgres.ports.main为5432(basePort)申请端口分配,并将实际分配结果写入settings.port与PGPORT环境变量;未开启网络监听时,该端口仅用于初始化阶段临时启动实例。仓库中的 tests/postgres-localhost/devenv.nix 展示了指定listen_addresses = "localhost"与port = 2345的写法,tests/postgres-pghost/devenv.nix 则演示了listen_addresses = "*"的场景。

postgresql.conf 调优:settings

属性值
类型属性集(attribute set of boolean/float/int/string)
默认值{}

直接对应 PostgreSQL 的postgresql.conf配置项。字符串值会被自动包裹在单引号中,且单引号会按上游文档规则转义为两个单引号。模块会将settings序列化为一份独立的postgresql.conf文件并覆盖到数据目录:

configFile = pkgs.writeText "postgresql.conf" (lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: "${n} = ${toStr v}") cfg.settings));

toStr的转换规则为:true→yes、false→no、字符串 → 单引号包裹、其余 →toString。官方示例:

settings = { log_connections = true; log_statement = "all"; logging_collector = true; log_disconnections = true; log_destination = lib.mkForce "syslog"; };

另外模块会在settings中强制注入三个键:listen_addresses(取自对应选项)、port(取自分配结果)以及默认值unix_socket_directories = $DEVENV_RUNTIME/postgres,保证 Unix socket 落在运行时目录。多进程场景下,注意同名 socket 目录会导致所有 PostgreSQL 实例共用 socket 路径,这在仓库的 postgres 相关测试中已被显式验证。

initdb 与客户端认证:initdbArgs、hbaConf

services.postgres.initdbArgs

属性值
类型字符串列表(行拼接)
默认值["--locale=C" "--encoding=UTF8"]
示例["--data-checksums" "--allow-group-access"]

数据目录初始化时额外传给initdb的参数。默认的--locale=C --encoding=UTF8保证了可复现的默认排序规则;需要数据校验和或允许组访问时可追加示例中的参数。

services.postgres.hbaConf

属性值
类型null or string
默认值null
示例builtins.readFile ./my-custom/directory/to/pg_hba.conf

自定义pg_hba.conf文件内容,会被拷贝进 PostgreSQL 安装目录,用于建立自定义的连接认证规则。实现中若该选项非空,会在初始化脚本里执行cp ${file} "$PGDATA/pg_hba.conf"。典型用途是从项目目录读取一份受版本控制的认证配置文件,例如:

services.postgres.hbaConf = builtins.readFile ./pg_hba.conf;

扩展管理:extensions

属性值
类型null 或(extensions -> list of package)函数
默认值null
示例extensions: [ extensions.pg_cron extensions.postgis extensions.timescaledb ]

声明式安装 PostgreSQL 扩展。可用扩展来自当前 nixpkgs 中postgresql.pkgs的属性集合,涵盖age、citus、hypopg、pg_cron、pg_net、pg_partman、pg_uuidv7、pgaudit、pgjwt、pgvector、pgvecto-rs、postgis、timescaledb、timescaledb_toolkit、sqlite_fdw、wal2json等 70 余项(完整清单以devenv eval求值结果为准)。

实现上,扩展通过package.withPackages机制注入:

postgresPkg = if cfg.extensions != null then if builtins.hasAttr "withPackages" cfg.package then cfg.package.withPackages cfg.extensions else builtins.throw '' Cannot add extensions to the PostgreSQL package. `services.postgres.package` is missing the `withPackages` attribute. Did you already add extensions to the package? '' else cfg.package;

也就是说,当你同时指定了自定义package时,该包必须带withPackages属性,否则会直接抛错。仓库示例 examples/postgres/devenv.nix 给出了“postgis + 建库 + initialScript 建扩展”的完整组合:

{ pkgs, ... }: { packages = [ pkgs.coreutils ]; services.postgres = { enable = true; extensions = extensions: [ extensions.postgis ]; initialDatabases = [ { name = "mydb"; } ]; initialScript = '' CREATE EXTENSION IF NOT EXISTS postgis; ''; }; }

首次启动初始化:initialDatabases 与 initialScript

services.postgres.initialDatabases

属性值
类型list of submodule
默认值[]

首次启动 PostgreSQL 时创建的数据库列表及其初始 schema。列表为空时退化为createDatabase行为。官方示例:

initialDatabases = [ { name = "foodatabase"; schema = ./foodatabase.sql; } { name = "bardatabase"; } ];

每个数据库条目包含以下子选项:

initialDatabases.*.name
  • 类型:string
  • 说明:要创建的数据库名称。
initialDatabases.*.schema
  • 类型:null or absolute path(types.path)
  • 默认值:null
  • 说明:数据库的初始 schema;为null(默认)时创建空数据库。

schema的实现支持两种形态,均由setupInitialDatabases处理:

  • 单个.sql文件:awk 'NF' file | psql --dbname ${database.name}逐条执行;
  • 目录:按版本顺序(ls -1v)读取目录下所有*.sql文件,逐个应用——这解决了“文件最后一条语句不以;结尾”时的执行问题。

仓库测试 tests/postgres-customdbuser/devenv.nix 正是使用了schema = ./.;(当前目录下的 SQL 文件按版本序应用)。

initialDatabases.*.user
  • 类型:null or string
  • 默认值:null
  • 说明:数据库属主用户名。若设置,会创建同名角色并使数据库归其所有;为null时默认使用$USER。
initialDatabases.*.pass
  • 类型:null or string
  • 默认值:null
  • 说明:数据库属主角色的密码,要求必须同时设置user。实现中CREATE ROLE "${user}" WITH LOGIN PASSWORD '${pass}'通过DO $$ ... EXCEPTION WHEN duplicate_object THEN RAISE NOTICE ...保证角色已存在时不报错;并且模块在assertions中做了硬性校验:pass非空而user为空时抛错(该行为是 2026 年起的变更,此前pass无user会被静默忽略)。

另一个相关行为变更:当在initialDatabases中指定user时,建库语句会带上OWNER,即CREATE DATABASE "name" OWNER "user",数据库不再一律归$USER所有。

initialDatabases.*.initialSQL
  • 类型:null or string
  • 默认值:null
  • 说明:在该数据库初始化期间运行的 SQL 命令,多条语句可用分号分隔。示例:
initialSQL = '' CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT); INSERT INTO users (name) VALUES ('admin'); CREATE EXTENSION IF NOT EXISTS pg_uuidv7; '';

仓库测试 tests/postgres-customperdbinit/devenv.nix 展示了 per-database 初始化的完整形态:第一个库testdb指定了user、pass与initialSQL(建扩展、建表、改属主),第二个库testdb2仅创建空库:

initialDatabases = [ { name = "testdb"; user = "testuser"; pass = "testuserpass"; initialSQL = '' CREATE EXTENSION IF NOT EXISTS pg_uuidv7; CREATE TABLE user_owned_table (id SERIAL PRIMARY KEY, name TEXT); ALTER TABLE user_owned_table OWNER TO testuser; ''; } { name = "testdb2"; } ];

services.postgres.initialScript

属性值
类型null or string
默认值null
示例"CREATE ROLE postgres SUPERUSER; CREATE ROLE bar;"

初始化期间运行的服务器级 SQL 命令(可分号分隔多条)。适用场景区分:

  • initialScript用于服务器级初始化,例如创建角色、配置全局设置;
  • initialSQL(在initialDatabases内)用于数据库级初始化;
  • initialScript在initialDatabases全部建库完成后执行。

实现中的执行顺序(见setupScript):首次启动 →initdb→ 拷贝配置文件与pg_hba.conf→ 临时用 Unix socket 启动实例(pg_ctl -w start)→ 执行setupInitialDatabases→ 执行runInitialScript→pg_ctl -m fast -w stop。由于initialDatabases与initialScript只在首次初始化($PGDATA不存在)时运行,后续重启不会重复执行。

环境变量、数据目录与进程生命周期

启用services.postgres后,模块自动注入以下环境变量:

环境变量取值
PGDATA$DEVENV_STATE/postgres(数据目录)
PGHOST网络监听地址;未监听时为$DEVENV_RUNTIME/postgres(socket 目录)
PGPORT分配后的端口

数据目录固定在$DEVENV_STATE/postgres。由于所有初始化逻辑都以$PGDATA是否已存在为判断条件(初始化完成后还会写入$PGDATA/.devenv_initialized标记文件),想重新执行初始化时,需要删除该状态目录(例如通过devenv gc或手动清理$DEVENV_STATE/postgres)再执行devenv up。

进程本身的定义位于processes.postgres:

  • exec:startScript,内部先执行setupScript完成初始化与配置,再exec postgres;
  • ready探针:检查.devenv_initialized标记,随后用pg_isready -d template1与psql -c "SELECT 1" template1双重验证实例可用;initial_delay = 2、probe_timeout = 4、failure_threshold = 5;
  • shutdown.signal = 2(SIGINT):对应 PostgreSQL 文档中的快速关停(fast shutdown)语义。

实战组合与常见注意事项

1. 本地开发 + TCP 访问(参考 tests/postgres-localhost/devenv.nix):

services.postgres = { enable = true; listen_addresses = "localhost"; port = 2345; initialScript = '' CREATE USER postgres SUPERUSER; ''; };

此时应用可通过psql -h localhost -p 2345或$PGHOST/$PGPORT连接,同时仍保留 Unix socket 访问路径。

2. 容器/远程场景开放全网监听(参考 tests/postgres-pghost/devenv.nix):

services.postgres.listen_addresses = "*";

注意*会让PGHOST解析为127.0.0.1,而服务端实际监听所有接口;如需限制访问请配合hbaConf定制认证规则。

3. 数据校验与多实例:可通过initdbArgs = ["--data-checksums"]开启数据页校验和。如果需要在同一devenv up中运行多个 PostgreSQL 实例,留意它们共享$DEVENV_STATE/postgres与运行时 socket 目录,需自行调整settings与端口分配策略,这在当前模块中是已知约束。

4. 状态清理:调整initialScript、initialDatabases、hbaConf、settings等仅在初始化/启动阶段生效的选项后,服务状态不会自动重建。请删除$DEVENV_STATE/postgres目录后重新devenv up,确保配置变更真正生效(详见 docs/src/content/docs/services/index.md 的说明)。

5. 版本与扩展联动:指定非默认package(如pkgs.postgresql_15)时,扩展集合也来自同一 nixpkgs 的postgresql.pkgs,二者版本需相互匹配;若package缺少withPackages属性,启用extensions会在求值时直接抛错(错误信息见 src/modules/services/postgres.nix)。

如需在模块化/多项目场景中复用这些配置,可以结合 devenv 的模块与 profile 机制(见 docs/src/content/docs/blog/2025/09/17/devenv-19-scaling-nix-projects-using-modules-and-profiles.md 中lib.mkIf config.myteam.services.database.enable的条件启用写法),把services.postgres封装成团队级可开关的数据库能力。

  • 开发工具
  • CLI

【免费下载链接】devenv

Fast, Declarative, Reproducible, and Composable Developer Environments using Nix

项目地址:https://gitcode.com/gh_mirrors/de/devenv
点击查看免费下载
上一篇:探索未来之路:2025年夏季实习宝典 —— Ouckah & CSCareers 携手启航
下一篇:CVAT 标注平台:一条命令部署,覆盖图像、视频与 3D 点云标注

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询