Skip to content
Self-Hosting

自定义 Postgres 扩展

Build a custom Docker image with additional Postgres extensions.

概览 #

🌐 Overview

supabase/postgres 镜像包含了一套在构建时编译好的精选扩展。没有运行时机制可以将一个已编译的原生 .so 扩展安装到正在运行的容器中:你不能 apk addapt-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.

添加扩展的正确方式是将它编译到镜像的 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.

为什么 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:

  1. 运行时是 glibc,而不是 musl。Nix 构建的 postgres 及其扩展是链接到 glibc 的。如果你用 Alpine 自带的工具链 (apk add build-base) 编译扩展,它会链接 musl,并在 CREATE EXTENSION 加载时失败,出现类似 libc.musl-aarch64.so.1: cannot open shared object file 的错误。你必须在 glibc 环境中构建。
  2. 匹配主版本,并保持构建器的 glibc 不晚于镜像的版本。扩展在整个 Postgres 主版本中 ABI 是稳定的。然而,如果扩展是基于比运行时提供的 glibc 更新的版本构建的,它将无法加载(version 'GLIBC_2.xx' not found)。Supabase 镜像目前提供 glibc 2.40,所以本指南基于 Debian 12(postgres:17-bookwormglibc 2.36)构建,这样安全性更高。避免使用默认的 postgres:17 —— 它目前是 Debian 13 和 glibc 2.41
  3. 模块目录被重定向了。正在运行的 postgres 是一个封装脚本,它通过 NIX_PGLIBDIR 环境变量覆盖其库目录。你编译的 .so 必须安装到那个目录里——而不是 pg_config --pkglibdir 显示的路径。这就是为什么你不能按照扩展的上游安装说明操作,那些说明通常会使用 make install 或复制到 pg_config --pkglibdir

先决条件 #

🌐 Prerequisites

  • Docker 已安装并运行
  • 你部署使用的确切 supabase/postgres 标签。例如,supabase/postgres:17.6.1.136
  • 使用标准 PGXS 构建的扩展源

构建扩展 #

🌐 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.

Dockerfile
1
# syntax=docker/dockerfile:1
2
ARG SUPABASE_POSTGRES_TAG=17.6.1.136
3
ARG PG_MAJOR=17
4
5
# --- Builder: glibc image matching the Postgres major version ---
6
FROM postgres:${PG_MAJOR}-bookworm AS builder
7
ARG PG_MAJOR
8
ARG EXT_VERSION=v1.7.0
9
RUN 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/*
12
WORKDIR /src
13
RUN git clone --depth 1 --branch ${EXT_VERSION} https://github.com/fboulnois/pg_uuidv7 .
14
RUN make
15
16
# --- Runtime: Supabase Alpine image ---
17
FROM supabase/postgres:${SUPABASE_POSTGRES_TAG}
18
USER root
19
COPY --from=builder /src/pg_uuidv7.so /tmp/
20
COPY --from=builder /src/pg_uuidv7.control /tmp/
21
COPY --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.
24
RUN 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 进程中加载,所以会自动解析。扩展链接的任何其他共享库(libcurlOpenSSL 等)也必须在运行时存在并可加载。

🌐 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:

1
docker build --target builder -t ext-builder .
2
docker run --rm --entrypoint sh ext-builder -c 'readelf -d /src/*.so | grep NEEDED'
  • 只有 libc.so.6ld-linux-* —— 上面的 Dockerfile 就足够了(pg_uuidv7 就是这种情况)。
  • 其他列出的东西——用下面的其中一个选项来处理这些依赖。

推荐:将额外的依赖静态链接到 .so 中,这样它剩下的唯一 NEEDED 就是 libc.so.6。构建完成后,重新运行上面的检查,确认只剩下 libc.so.6ld-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:

1
docker 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:

docker-compose.yml
1
db:
2
image: supabase-postgres-custom:17.6.1.136
3
# ...leave the rest of the service definition unchanged

然后重新创建数据库服务,这样它就会使用新的镜像:

🌐 Then recreate the database service so it picks up the new image:

1
sh 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/.

1
docker exec supabase-db bash -c '
2
CUR=$(psql -U postgres -tAc "show supautils.privileged_extensions" | tr -d "\n")
3
cat > /etc/postgresql-custom/conf.d/99-custom-extensions.conf <<EOF
4
supautils.privileged_extensions = '"'"'$CUR, pg_uuidv7'"'"'
5
EOF'

重启以应用,然后像创建普通 postgres 角色一样创建它:

🌐 Restart to apply, then create it as the regular postgres role:

1
sh run.sh restart db
2
docker compose exec db psql -U postgres -c "CREATE EXTENSION pg_uuidv7;"

选项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:

docker-compose.yml
1
db:
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:Z
volumes/db/pg_uuidv7.sql
1
CREATE EXTENSION IF NOT EXISTS pg_uuidv7;

核实扩展 #

🌐 Verify the extension

1
docker compose exec db psql -U postgres \
2
-c "CREATE EXTENSION IF NOT EXISTS pg_uuidv7;" \
3
-c "SELECT uuid_generate_v7();"
1
uuid_generate_v7
2
--------------------------------------
3
019f897b-1003-7175-b9b5-be46fc0c3990
4
(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:

1
docker exec supabase-db bash -c '
2
CUR=$(psql -U postgres -tAc "show shared_preload_libraries" | tr -d "\n")
3
cat > /etc/postgresql-custom/conf.d/99-preload.conf <<EOF
4
shared_preload_libraries = '"'"'$CUR, custom_bgworker'"'"'
5
EOF'
6
sh run.sh restart db

把扩展构建进 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:

保持图片更新 #

🌐 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 时发生的那样——可能需要调整构建。