通常安装PostgreSQL需要root权限安装后默认用专用账户postgres访问使用专用的目录/var/lib/postgresql/存储数据。服务的配置和启动由系统服务管理器管理其控制通常需要root权限。全过程涉及多个用户、多个进程操作较为复杂。基于apptainer旧名singularity的容器化方案允许普通用户在个人目录下独立创建和管理多个数据库实例能够极大地方便普通用户维护和调试数据库。本文的目标是利用容器化方案让普通用户在没有安装PostgreSQL的主机上在用户目录下独立快速创建和维护一个PostgreSQL数据库并使用PostgREST使其可以通过RESTful API访问。容器镜像的制作在空目录下创建两个文本文件Dockerfile或apptainer.def和entrypoint.sh并clone GitHub仓库pgjwt。Dockerfile指示了容器构建方法。我们以Debian 13下的postgres 18官方容器postgres:18-trixie为基础从postgrest/postgrest:latest镜像获取postgrest二进制文件。为了使用PostgREST我们还需要安装pgjwt来为PostgreSQL提供jwt扩展支持。此外我们还将指定必要的环境变量来为后续的命令提供方便。# 基础镜像FROM postgres:18-trixie# 安装 makeRUN apt-get update \ apt-get install -y build-essential \ rm -rf /var/lib/apt/lists/*# 提取 postgrestCOPY --frompostgrest/postgrest:latest /bin/postgrest /usr/local/bin/postgrest# 安装 pgjwtCOPY pgjwt /usr/src/pgjwtRUN make -C /usr/src/pgjwt install# 设置环境变量ENV PGDATA/var/lib/postgresqlENV PGHOST/var/lib/postgresql/socketENV PGDATABASEmain# 设置入口脚本COPY entrypoint.sh /usr/local/bin/entrypoint.shRUN chmod x /usr/local/bin/entrypoint.shENTRYPOINT [/usr/local/bin/entrypoint.sh]# 设置默认命令可选CMD [psql]该Dockerfile中还指定了数据库文件目录PGDATA、创建socket目录PGHOST并将默认的database名设置为main。此外还指定了入口脚本entrypoint.sh以及默认的启动命令psql。如果不想使用docker也可以直接通过apptainer.def进行镜像定义点击展开entrypoint.sh是容器运行时会调用的脚本用于在执行用户命令前按需初始化数据库并启动postgres服务并在命令结束后终止postgres服务。#!/bin/bashset -eif [ ! -f $PGDATA/PG_VERSION ]; then # 如果数据文件不存在initdb 2 # 初始化数据库文件mkdir -p $PGHOST # 创建socket目录sed -i s/^[# ]*listen_addresses\s*.*/listen_addresses / $PGDATA/postgresql.conf # 禁用端口监听只通过socket方式访问echo unix_socket_directories $PGHOST $PGDATA/postgresql.conf # 指定socket目录pg_ctl start 2 # 启动数据库服务createdb # 创建数据库elsepg_ctl start 2 # 启动数据库服务fitrap pg_ctl stop 2 EXIT # 退出时停止数据库服务$ # 执行传入命令用docker构建容器之后在apptainer中拉取为保存为sif镜像文件# 利用 docker 构建docker build -t pg18-postgrest:latest .apptainer build pg18-postgrest.sif docker-daemon://pg18-postgrest:latest# 或者直接从 apptainer.def 文件创建镜像apptainer build pg18-postgrest.sif apptainer.def这样就制作完成了一个包含PostgreSQL和PostgREST的镜像pg18-postgrest.sif。容器镜像的使用使用容器镜像部署PostgreSQL数据库我们构建镜像时设置了数据库文件在镜像内的目录PGDATA /var/lib/postgresql为了让其中的内容在镜像运行结束后依然能保存在硬盘上我们需要通过--bind将本地目录“挂载”到镜像的/var/lib/postgresql目录下。利用singularity run启动镜像并运行命令如psql入口脚本会自动以挂载的目录为数据库数据目录启动postgresql如果检测到数据目录为空时还会自动初始化。singularity run --bind /path/to/database/:/var/lib/postgresql pg18-postgrest.sif psql运行上述命令后会进入psql命令行界面退出后可以看到/path/to/database/下自动创建了postgresql相关数据文件。之后再次运行可以在psql中对位于/path/to/database/的数据库进行操作例如查询、插入或删除表和数据。使用容器镜像运行PostgRESTPostgREST利用数据库中的结构约束和权限决定API端点和操作。RequestResponseQueryResultWeb ClientPostgRESTPostgreSQLPostgREST的认证涉及数据库、PostgREST和web客户端三方PostgREST同时作为数据库客户端和web服务端需要在两套机制之间进行转换较为复杂。具体而言PostgREST利用配置中给定的用户名、密码和登录方式访问数据库通过jwt验证web客户端所声称的身份验证通过后以其声称的身份对数据库数据进行读取或修改。其中任何一个环节权限校验失败都会导致错误。jwtJSON Web Tokens是一段纯文本字符串由三部分组成第一、二部分是base64编码的元数据指定签名算法和数据即web客户端“声称”的身份第三部分是利用一个密钥字符串对前两部分签名后的结果持有密钥者可以根据密钥字符串验证jwt的合法性以拒绝第三部分签名不合要求的访问请求。PostgREST本身只会校验jwt的合法性不带有认证用户或签发token的能力。为了让web客户端能够向PostgREST提供正确签名的jwt有两种手段一是向可信的web客户端分发密钥字符串由客户端自行签名构造jwt。一旦如此web客户端就可以任意“声称”身份访问数据库。二是由数据库根据web客户端传递的数据如用户名、密码签发jwtweb客户端只能以数据库签发的身份访问数据库。第二种手段允许我们向不同的web客户端签发不同权限的jwt以实现差异化授权。为此需要在PostgreSQL数据库中定义一个认证函数login。下面的sql展示了一套适用于PostgREST的数据库定义-- setup.sql-- 启用 pgjwt 扩展CREATE EXTENSION IF NOT EXISTS pgcrypto;CREATE EXTENSION IF NOT EXISTS pgjwt;-- 创建用户用于postgrest访问CREATE USER postgrest NOINHERIT LOGIN PASSWORD mypassword; -- 用于postgrest登录否则postgrest无法连接到数据库CREATE ROLE web_anon NOLOGIN;CREATE ROLE web_auth NOLOGIN; -- 允许web客户端声称不同的身份以实现差异化授权GRANT web_anon, web_auth TO postgrest; -- 允许postgrest登录后以web_anon或web_auth的身份执行操作CREATE SCHEMA data;-- 认证函数用于签发jwtCREATE OR REPLACE FUNCTION data.login(username text, password text)RETURNS text AS $$DECLAREsecret text : ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789; -- 密钥字符串BEGIN-- 生产环境中会根据传入的username和password进行认证判断-- 这里略过了认证步骤对任意访问者签发web_auth身份的永久令牌-- 生产环境中通常会在令牌payload中包含过期时间字段(exp)RETURN sign(json_build_object(role, web_auth -- 签发的身份--,exp, extract(epoch from now() interval 1 hour) -- 过期时间), secret);END;$$ LANGUAGE plpgsql SECURITY DEFINER;-- 创建表格CREATE TABLE data.users (uid bigint GENERATED ALWAYS AS IDENTITY primary key,username text not null,department text);-- 设置访问权限GRANT USAGE ON SCHEMA data TO web_anon, web_auth;GRANT execute ON FUNCTION data.login(text, text) TO web_anon; -- 允许普通身份申请jwtGRANT select ON ALL TABLES IN SCHEMA data TO web_anon; -- 允许普通身份查询数据GRANT select, insert, update, delete ON ALL TABLES IN SCHEMA data TO web_auth; -- 允许web_auth身份修改数据-- 导入数据这里用3条测试记录作为示例INSERT INTO data.users (username, department) VALUES(Alice, Engineering),(Bob, Marketing),(Charlie, Human Resources);将上述内容导入PostgreSQLcat setup.sql | singularity run --bind /path/to/database/:/var/lib/postgresql pg18-postgrest.sif psql -f -新建一个postgrest.conf文件db-uri postgres://postgrest:mypassword/main?host/var/lib/postgresql/socketdb-schemas datadb-anon-role web_anonjwt-secret ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789server-port 3000log-level info这里的db-schemas是允许访问的schemadb-anon-role指定了没有传递jwt时访问数据库时的默认身份。PostgREST会通过jwt-secret密钥校验请求传递的jwt。之后即可通过PostgREST启动RESTful APIsingularity run --bind /path/to/database/:/var/lib/postgresql pg18-postgrest.sif postgrest postgrest.conf访问postgrest RESTful API可以通过任意http客户端访问PostgREST RESTful API这里以命令行客户端curl为例用GET方法获取数据curl http://localhost:3000/users返回[{uid:1,username:Alice,department:Engineering},{uid:2,username:Bob,department:Marketing},{uid:3,username:Charlie,department:Human Resources}]从login端点获取jwtcurl http://localhost:3000/rpc/login?usernameguestpassword123456返回eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIiA6ICJ3ZWJfYXV0aCJ9.WmyKgKms-SJ9unFwSpOzqGLFVVAN6iO9sKYR2hh_KKQ利用签发的jwt我们可以以web_auth的身份访问数据库并通过POST方法插入行、PATCH方法修改行、DELETE方法删除行。# POST: 插入行curl -X POST http://localhost:3000/users \-H Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIiA6ICJ3ZWJfYXV0aCJ9.WmyKgKms-SJ9unFwSpOzqGLFVVAN6iO9sKYR2hh_KKQ \-H Content-Type: application/json \-d {username:Dave,department:Engineering}# PATCH: 修改行curl -X PATCH http://localhost:3000/users?usernameeq.Alice \-H Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIiA6ICJ3ZWJfYXV0aCJ9.WmyKgKms-SJ9unFwSpOzqGLFVVAN6iO9sKYR2hh_KKQ \-H Content-Type: application/json \-d {department:Marketing}# DELETE: 删除行curl -X DELETE http://localhost:3000/users?usernameeq.Bob \-H Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIiA6ICJ3ZWJfYXV0aCJ9.WmyKgKms-SJ9unFwSpOzqGLFVVAN6iO9sKYR2hh_KKQ再次查询数据curl http://localhost:3000/users返回[{uid:3,username:Charlie,department:Human Resources},{uid:4,username:Dave,department:Engineering},{uid:1,username:Alice,department:Marketing}]