- 开发工具
- CLI
【免费下载链接】devenv
Fast, Declarative, Reproducible, and Composable Developer Environments using Nix
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
相关推荐
Node.js v7 升级 V8 5.4:新 ECMAScript 特性与性能优化解读
Node.js v7 升级 V8 5.4:新 ECMAScript 特性与性能优化解读 本文以 nodejs.org 仓库中的官方公告 update v8 5.
开发工具CLI革命性Go插件系统go-plugin:基于WebAssembly打造安全高效的插件生态
革命性Go插件系统go plugin:基于WebAssembly打造安全高效的插件生态 go plugin是一款基于WebAssembly技术的Go插件系统,它
开发工具CLIAgentMesh Kubernetes 部署实战:独立信任代理与 Sidecar 双模式接入指南(Agent Governance Toolkit)
AgentMesh Kubernetes 部署实战:独立信任代理与 Sidecar 双模式接入指南(Agent Governance Toolkit) 本文为
开发工具CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考