自定义 Postgres 扩展
Build a custom Docker image with additional Postgres extensions.
概览 #
🌐 Overview
supabase/postgres 镜像包含了一套在构建时编译好的精选扩展。没有运行时机制可以将一个已编译的原生 .so 扩展安装到正在运行的容器中:你不能 apk add 或 apt-get install 一个扩展包,因为镜像是用 Nix 构建的,而且它的扩展集在镜像生成时就固定了。
🌐 The supabase/postgres image includes a curated set of extensions that are compiled in at build time. There is no runtime mechanism to install a compiled native .so extension into a running container: you cannot apk add or apt-get install an extension package, because the image is built with Nix and its extension set is fixed when the image is produced.
要添加 Supabase Postgres 没有提供的本地扩展,你需要自己构建镜像。本指南将从头到尾讲解当前基于 Alpine 的镜像,使用 pg_uuidv7 作为运行示例。
🌐 To add a native extension that Supabase Postgres doesn't provide, you have to build your own image. This guide walks through that end to end for the current Alpine-based images, using pg_uuidv7 as the running example.
如果你的扩展是用纯 SQL 或受信任的过程语言编写的,你根本不需要自定义镜像。使用 pg_tle,它已经预装好且预加载了——运行 CREATE EXTENSION pg_tle; 并通过它安装你的扩展。
🌐 If your extension is written in pure SQL or a trusted procedural language, you don't need a custom image at all. Use pg_tle, which is already bundled and preloaded - run CREATE EXTENSION pg_tle; and install your extension through it.
添加扩展的正确方式是将它编译到镜像的 Nix build 中。这是最稳妥的方法,但它需要 Nix、从源代码构建镜像,并维护一个分支。本文的其余部分描述了一个更简单的替代方案:在普通的 Docker 构建器中构建你的扩展,然后将其叠加到发布的镜像上。这个方法适用于许多标准的 PGXS 扩展——不过效果可能会有所不同,下一节的限制就是这个折中的代价。
🌐 The proper way to add an extension is to compile it into the image's Nix build. That's the most robust route, but it requires Nix, building the image from source, and maintaining a fork. The rest of this guide describes a simpler alternative: build your extension in an ordinary Docker builder and layer it onto the published image. The approach can work for many standard PGXS extensions - but your mileage may vary, and the constraints in the next section are the trade-off.
Supabase 构建、测试并维护官方的 supabase/postgres 镜像及其打包的扩展。按照本指南构建的自定义镜像是非官方的且不受支持——不能保证其可用,并且将来基础镜像更改时可能会出问题。测试、质量保证和持续维护需要你自己负责。
🌐 Supabase builds, tests, and maintains the official supabase/postgres images and their bundled extensions. A custom image built by following this guide is unofficial and unsupported - it's not guaranteed to work, and may break with future changes to the base image. Testing, quality assurance, and ongoing maintenance are your responsibility.
为什么 Postgres 镜像需要特殊处理 #
🌐 Why Postgres images need special handling
基础镜像是 Alpine Linux,但 Postgres 二进制文件和所有打包的扩展都存放在 /nix 存储中:
🌐 The base image is Alpine Linux, but the Postgres binaries and every bundled extension live in a /nix store:
- 运行时是
glibc,而不是musl。Nix 构建的postgres及其扩展是链接到glibc的。如果你用 Alpine 自带的工具链 (apk add build-base) 编译扩展,它会链接musl,并在CREATE EXTENSION加载时失败,出现类似libc.musl-aarch64.so.1: cannot open shared object file的错误。你必须在glibc环境中构建。 - 匹配主版本,并保持构建器的
glibc不晚于镜像的版本。扩展在整个 Postgres 主版本中 ABI 是稳定的。然而,如果扩展是基于比运行时提供的glibc更新的版本构建的,它将无法加载(version 'GLIBC_2.xx' not found)。Supabase 镜像目前提供glibc 2.40,所以本指南基于 Debian 12(postgres:17-bookworm,glibc 2.36)构建,这样安全性更高。避免使用默认的postgres:17—— 它目前是 Debian 13 和glibc 2.41。 - 模块目录被重定向了。正在运行的
postgres是一个封装脚本,它通过NIX_PGLIBDIR环境变量覆盖其库目录。你编译的.so必须安装到那个目录里——而不是pg_config --pkglibdir显示的路径。这就是为什么你不能按照扩展的上游安装说明操作,那些说明通常会使用make install或复制到pg_config --pkglibdir。
图片是如何进行版本管理的
标签以 Postgres 版本开头,后面跟着 Supabase 自己的发行号。扩展 ABI 在一个主版本中是稳定的——但是如果将来 Supabase Postgres 镜像升级了 glibc,就需要重新检查第 2 点的规则。你可以这样读取镜像的 glibc 版本:
🌐 The tag starts with the Postgres version followed by Supabase's own release numbers. Extension ABI is stable across a major version - but if a future Supabase Postgres image bumps its glibc, re-check the rule in point 2. You can read the image's glibc version with:
1docker run --rm --entrypoint sh supabase/postgres:17.6.1.136 -c \2 'ls -d /nix/store/*glibc-2.*-* 2>/dev/null | grep -oE "glibc-2\.[0-9]+" | sort -uV | tail -1'先决条件 #
🌐 Prerequisites
- Docker 已安装并运行
- 你部署使用的确切
supabase/postgres标签。例如,supabase/postgres:17.6.1.136 - 使用标准 PGXS 构建的扩展源
总是针对你运行的相同主版本进行构建,每次升级基础镜像时都要重新构建你的自定义镜像。
🌐 Always build against the same major version you run, and rebuild your custom image whenever you upgrade the base image.
构建扩展 #
🌐 Build the extension
使用多阶段构建:先用一个 glibc 构建阶段(postgres:<major>-bookworm)进行编译,然后使用 Supabase 镜像作为运行阶段,将生成的文件安装到正确的 Nix 位置。
第1步:写 Dockerfile #
🌐 Step 1: Write the Dockerfile
下面的例子构建了从其 Git 仓库克隆的 pg_uuidv7。
🌐 The example below builds pg_uuidv7 cloned from its Git repository.
1# syntax=docker/dockerfile:12ARG SUPABASE_POSTGRES_TAG=17.6.1.1363ARG PG_MAJOR=1745# --- Builder: glibc image matching the Postgres major version ---6FROM postgres:${PG_MAJOR}-bookworm AS builder7ARG PG_MAJOR8ARG EXT_VERSION=v1.7.09RUN apt-get update && apt-get install -y --no-install-recommends \10 build-essential git ca-certificates postgresql-server-dev-${PG_MAJOR} \11 && rm -rf /var/lib/apt/lists/*12WORKDIR /src13RUN git clone --depth 1 --branch ${EXT_VERSION} https://github.com/fboulnois/pg_uuidv7 .14RUN make1516# --- Runtime: Supabase Alpine image ---17FROM supabase/postgres:${SUPABASE_POSTGRES_TAG}18USER root19COPY --from=builder /src/pg_uuidv7.so /tmp/20COPY --from=builder /src/pg_uuidv7.control /tmp/21COPY --from=builder /src/sql/ /tmp/ext-sql/22# The running postgres wrapper redirects its module dir via NIX_PGLIBDIR.23# Install the .so there; install control/SQL into pg_config --sharedir.24RUN set -eux; \25 PLUGIN_DIR="$(grep -E '^export NIX_PGLIBDIR' /usr/bin/postgres | sed -E "s/.*'([^']*)'.*/\1/")"; \26 SHARE_DIR="$(pg_config --sharedir)/extension"; \27 install -m 755 /tmp/pg_uuidv7.so "$PLUGIN_DIR/"; \28 install -m 644 /tmp/pg_uuidv7.control "$SHARE_DIR/"; \29 install -m 644 /tmp/ext-sql/*.sql "$SHARE_DIR/"; \30 rm -rf /tmp/pg_uuidv7.* /tmp/ext-sql相同的 Dockerfile 结构通常也适用于你自己的扩展。不过,当从零开始编写一个新扩展时,最好还是使用 Nix 构建(参考下文的 将扩展构建到 Nix 镜像中)。
🌐 The same Dockerfile structure generally works for your own extensions. However, when authoring a new extension from scratch, it's best to use the Nix build instead (refer to Build the extension into the Nix image below).
处理标准库以外的依赖 #
🌐 Handle dependencies beyond the standard library
只有当 .so 唯一的运行时依赖是 C 库(libc.so.6)时,把它复制到镜像上才能干净地工作——那就是镜像的 glibc,已经在 postgres 进程中加载,所以会自动解析。扩展链接的任何其他共享库(libcurl、OpenSSL 等)也必须在运行时存在并可加载。
🌐 Copying a .so onto the image works cleanly only when its sole runtime dependency is the C library (libc.so.6) - that's the image's glibc, already loaded in the postgres process, so it resolves automatically. Any other shared library the extension links (libcurl, OpenSSL, etc.) must also be present and loadable at runtime.
构建是在 Docker 内进行的——检查 builder 阶段编译的 .so,而不是在你的主机上:
🌐 The build happens inside Docker - check the compiled .so in the builder stage rather than on your host:
1docker build --target builder -t ext-builder .2docker run --rm --entrypoint sh ext-builder -c 'readelf -d /src/*.so | grep NEEDED'- 只有
libc.so.6和ld-linux-*—— 上面的 Dockerfile 就足够了(pg_uuidv7就是这种情况)。 - 其他列出的东西——用下面的其中一个选项来处理这些依赖。
推荐:将额外的依赖静态链接到 .so 中,这样它剩下的唯一 NEEDED 就是 libc.so.6。构建完成后,重新运行上面的检查,确认只剩下 libc.so.6 和 ld-linux-*。
🌐 Recommended: statically link the extra dependencies into the .so, so its only remaining NEEDED is libc.so.6. After building, re-run the check above and confirm only libc.so.6 and ld-linux-* remain.
有几件事让这比听起来复杂一些:
🌐 A few things make this more involved than it sounds:
- 发行版自带的静态归档通常不够用。例如,一个功能丰富的
libcurl.a包含许多依赖,而这些依赖的静态归档并不都能安装——所以你通常会从源代码构建依赖的最小静态版本然后链接它。 - PGXS 可能会忽略扩展的
CFLAGS。通过在make命令行使用PG_CPPFLAGS来传递额外的包含路径,而不是修改 Makefile。 - 静态链接是有局限的。它帮不了运行时
dlopen插件的依赖,而且如果链接一个同时被另一个扩展加载的库(比如一个进程里有两个 OpenSSL 副本),可能会冲突。 - 有些扩展会把自己的名字写死,所以你不能通过改名来避开和自带扩展的冲突。
当静态链接不切实际时,使用 Nix 路径,它会自动根据镜像自身的库解析所有依赖。
🌐 When static linking isn't practical, use the Nix path, which resolves every dependency against the image's own libraries automatically.
步骤 2:构建镜像 #
🌐 Step 2: Build the image
针对你在生产环境中运行的标签构建,这样运行时和主要版本就匹配了:
🌐 Build against the tag you run in production so the runtime and major version match:
1docker build \2 --build-arg SUPABASE_POSTGRES_TAG=17.6.1.136 \3 --build-arg PG_MAJOR=17 \4 -t supabase-postgres-custom:17.6.1.136 \5 .步骤3:在你的堆栈中使用这张图片 #
🌐 Step 3: Use the image in your stack
在 docker-compose.yml 中将 db 服务指向你的自定义镜像:
🌐 Point the db service at your custom image in docker-compose.yml:
1db:2 image: supabase-postgres-custom:17.6.1.1363 # ...leave the rest of the service definition unchanged然后重新创建数据库服务,这样它就会使用新的镜像:
🌐 Then recreate the database service so it picks up the new image:
1sh run.sh recreate db步骤 4:启用扩展 #
🌐 Step 4: Enable the extension
Supabase 的 postgres 角色故意不是超级用户,而且扩展创建受 supautils 限制。没有在 supautils.privileged_extensions 允许列表上的原生扩展只能由 supabase_admin 超级用户创建。你有两个选择。
🌐 The Supabase postgres role is intentionally not a superuser, and extension creation is gated by supautils. A native extension that isn't on the supautils.privileged_extensions allow-list can only be created by the supabase_admin superuser. You have two options.
选项A:允许 postgres#
🌐 Option A: Allow the postgres role to create it (no rebuild)
在自定义 supautils 配置中将你的扩展追加到允许列表。Postgres 17 镜像会从 /etc/postgresql-custom/conf.d/ 加载任何 .conf 文件。
🌐 Append your extension to the allow-list in a custom supautils configuration. The Postgres 17 image loads any .conf file from /etc/postgresql-custom/conf.d/.
1docker exec supabase-db bash -c '2CUR=$(psql -U postgres -tAc "show supautils.privileged_extensions" | tr -d "\n")3cat > /etc/postgresql-custom/conf.d/99-custom-extensions.conf <<EOF4supautils.privileged_extensions = '"'"'$CUR, pg_uuidv7'"'"'5EOF'重启以应用,然后像创建普通 postgres 角色一样创建它:
🌐 Restart to apply, then create it as the regular postgres role:
1sh run.sh restart db2docker compose exec db psql -U postgres -c "CREATE EXTENSION pg_uuidv7;"/etc/postgresql-custom/ 位于名为 db-config 的卷上,所以这个更改在重启后仍然有效。阅读 自定义 Postgres 配置 以了解更多关于 conf.d/ 机制的信息。
选项B:以超级用户身份启用它 #
🌐 Option B: Enable it as a superuser
镜像的初始化脚本在首次启动时以 supabase_admin(超级用户)身份运行。将 SQL 文件放入初始化目录,以便为新数据库自动创建扩展:
🌐 The image's init scripts run as supabase_admin (a superuser) on first boot. Drop a SQL file into the init directory to create the extension automatically for new databases:
1db:2 volumes:3 # ...keep the existing mounts (the data dir, roles.sql, etc.) and add:4 - ./volumes/db/pg_uuidv7.sql:/docker-entrypoint-initdb.d/migrations/99-pg_uuidv7.sql:Z1CREATE EXTENSION IF NOT EXISTS pg_uuidv7;初始化脚本只会在数据目录(./volumes/db/data)为空时运行,也就是第一次初始化的时候。对于已经初始化的数据库,可以用 supabase_admin 连接一次,然后手动运行 CREATE EXTENSION。
🌐 Init scripts only run when the data directory (./volumes/db/data) is empty, that is on first initialization. For an already-initialized database, connect as supabase_admin once and run CREATE EXTENSION manually.
核实扩展 #
🌐 Verify the extension
1docker compose exec db psql -U postgres \2 -c "CREATE EXTENSION IF NOT EXISTS pg_uuidv7;" \3 -c "SELECT uuid_generate_v7();"1uuid_generate_v72--------------------------------------3 019f897b-1003-7175-b9b5-be46fc0c39904(1 row)必须预先加载的扩展 #
🌐 Extensions that have to be preloaded
如果你的扩展必须列在 shared_preload_libraries 中,用相同的 conf.d/ 机制添加它。conf.d/ 会在内置设置之后运行,所以需要重述当前值加上你的库:
🌐 If your extension must be listed in shared_preload_libraries, add it with the same conf.d/ mechanism. The conf.d/ include runs after the baked-in setting, so restate the current value plus your library:
1docker exec supabase-db bash -c '2CUR=$(psql -U postgres -tAc "show shared_preload_libraries" | tr -d "\n")3cat > /etc/postgresql-custom/conf.d/99-preload.conf <<EOF4shared_preload_libraries = '"'"'$CUR, custom_bgworker'"'"'5EOF'6sh run.sh restart dbshared_preload_libraries 没有追加语法。请把现有列表全部加上再加上你的库,否则你会禁用 Supabase 依赖的扩展。
把扩展构建进 Nix 镜像里 #
🌐 Build the extension into the Nix image
上面提到的 Docker 方法是把单独编译的 .so 添加到已发布的镜像上。最可靠的方法是把这个扩展加到 Supabase 自己的 Nix 构建里,这样它就能和镜像里的其他东西用完全相同的工具链编译。这种方法完全绕过了 libc/ABI 的问题,也是官方打包扩展的构建方式。它需要 Nix、从源码构建镜像,并且维护一个 supabase/postgres 的分支。
🌐 The Docker approach above layers a separately-compiled .so onto the published image. The most reliable way is to add the extension to Supabase's own Nix build, so it's compiled with the exact same toolchain as everything else in the image. This option sidesteps the libc/ABI concerns entirely and is how the bundled extensions are built. It requires Nix, building the image from source, and maintaining a fork of supabase/postgres.
选择指南大概如下:
🌐 Rough guide to choosing:
- 一次性扩展库存镜像,工具最少——本指南中的 Docker 方法。
- 防弹 ABI 匹配,想要合并到上游,或者需要按 “Supabase 方式” 做 preload /
supautils接线——下面是 Nix 构建。
本指南故意不教 Nix。权威的、持续维护的说明在 supabase/postgres 仓库里:
🌐 This guide intentionally doesn't teach Nix. The authoritative, maintained instructions live in the supabase/postgres repository:
- 添加一个新的扩展包 - 主要操作指南(C/C++ 和 Rust/pgrx 模式,扩展的注册位置,生成哈希)
- 创建一个
pgrx扩展 - 用于 Rust 扩展 - 构建 Postgres 以及完整的
nix/docs目录
保持图片更新 #
🌐 Keeping the image up to date
因为你的镜像固定在特定的 supabase/postgres 标签上,并且依赖于该构建的内部 Nix 路径,所以把它当作与基础镜像绑定的:
🌐 Because your image is pinned to a specific supabase/postgres tag and depends on that build's internal Nix paths, treat it as coupled to the base image:
- 每次升级 Postgres 时,都用新的
SUPABASE_POSTGRES_TAG(以及对应的PG_MAJOR)重建。 - 在每次重建后重新测试
CREATE EXTENSION。基础操作系统或 Nix 布局的变化——就像当镜像从 Debian 换到 Alpine 时发生的那样——可能需要调整构建。