Skip to content

Commit 532e8f2

Browse files
committed
feat: add pg_duckdb extension with role and access grants
1 parent 041eb41 commit 532e8f2

4 files changed

Lines changed: 268 additions & 3 deletions

File tree

Dockerfile-17-fast

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# syntax=docker/dockerfile:1.6
2+
# Alpine-based slim PostgreSQL 17 image with Nix extensions
3+
# Fast build variant: uses a BuildKit cache mount to persist the Nix store between builds.
4+
# First build still downloads packages; subsequent builds skip downloads entirely.
5+
6+
####################
7+
# Stage 1: Nix builder
8+
####################
9+
FROM alpine:3.21 AS nix-builder
10+
11+
# Install dependencies for nix installer (coreutils for GNU cp, sudo for installer)
12+
RUN apk add --no-cache \
13+
bash \
14+
coreutils \
15+
curl \
16+
shadow \
17+
sudo \
18+
xz
19+
20+
# Create users (Alpine syntax)
21+
RUN addgroup -S postgres && \
22+
adduser -S -h /var/lib/postgresql -s /bin/bash -G postgres postgres && \
23+
addgroup -S wal-g && \
24+
adduser -S -s /bin/bash -G wal-g wal-g
25+
26+
WORKDIR /nixpg
27+
COPY . .
28+
29+
# Build PostgreSQL and groonga with extensions.
30+
#
31+
# --mount=type=cache persists /nix between builds on this machine so Nix does not
32+
# re-download or rebuild packages that are already in the store. The cache is keyed
33+
# by id so it is shared across invalidated layers (e.g. when COPY . . changes).
34+
#
35+
# Because cache-mount contents are NOT committed to the image layer we copy the
36+
# finished store to /nix-output at the end; the production stage COPYs from there.
37+
#
38+
# Nix is installed in single-user (--no-daemon) mode so the entire store lives
39+
# under /nix (the cache mount) and needs no daemon process.
40+
RUN --mount=type=cache,id=psql17-nix-store,target=/nix,sharing=locked \
41+
sh -c ' \
42+
set -eu; \
43+
\
44+
# Write nix.conf every time (it lives in the writable layer, not the cache mount). \
45+
# build-users-group must be empty so the single-user installer works as root; \
46+
# sandbox=false is required inside Docker containers. \
47+
mkdir -p /etc/nix; \
48+
printf "build-users-group = \nsandbox = false\nextra-experimental-features = nix-command flakes\nextra-substituters = https://nix-postgres-artifacts.s3.amazonaws.com\nextra-trusted-public-keys = nix-postgres-artifacts:dGZlQOvKcNEjvT7QEAJbcV6b6uk7VF/hWMjhYleiaLI=\n" > /etc/nix/nix.conf; \
49+
\
50+
if [ ! -f /nix/var/nix/profiles/default/bin/nix ]; then \
51+
curl -L https://releases.nixos.org/nix/nix-2.33.2/install | sh -s -- --no-daemon --no-channel-add; \
52+
fi; \
53+
\
54+
export PATH="/nix/var/nix/profiles/default/bin:$PATH"; \
55+
\
56+
nix profile add path:.#psql_17_slim/bin; \
57+
nix store gc; \
58+
\
59+
nix profile add path:.#supabase-groonga; \
60+
nix store gc; \
61+
\
62+
# Copy the store out of the cache mount so it is committed to the image layer. \
63+
mkdir -p /nix-output; \
64+
cp -a /nix/. /nix-output/; \
65+
' && \
66+
mkdir -p /tmp/groonga-plugins && \
67+
cp -r /nix-output/var/nix/profiles/default/lib/groonga/plugins /tmp/groonga-plugins/
68+
69+
####################
70+
# Stage 2: Gosu builder
71+
####################
72+
FROM alpine:3.21 AS gosu-builder
73+
74+
ARG TARGETARCH
75+
ARG GOSU_VERSION=1.16
76+
77+
RUN apk add --no-cache gnupg curl
78+
79+
# Download and verify gosu
80+
RUN curl -fsSL "https://github.com/tianon/gosu/releases/download/${GOSU_VERSION}/gosu-${TARGETARCH}" -o /usr/local/bin/gosu && \
81+
curl -fsSL "https://github.com/tianon/gosu/releases/download/${GOSU_VERSION}/gosu-${TARGETARCH}.asc" -o /usr/local/bin/gosu.asc && \
82+
GNUPGHOME="$(mktemp -d)" && \
83+
export GNUPGHOME && \
84+
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4 && \
85+
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu && \
86+
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc && \
87+
chmod +x /usr/local/bin/gosu
88+
89+
####################
90+
# Stage 3: Final production image
91+
####################
92+
FROM alpine:3.21 AS production
93+
94+
# Install minimal runtime dependencies
95+
RUN apk add --no-cache \
96+
bash \
97+
curl \
98+
shadow \
99+
su-exec \
100+
tzdata \
101+
musl-locales \
102+
musl-locales-lang \
103+
&& rm -rf /var/cache/apk/*
104+
105+
# Create postgres user/group
106+
RUN addgroup -S postgres && \
107+
adduser -S -G postgres -h /var/lib/postgresql -s /bin/bash postgres && \
108+
addgroup -S wal-g && \
109+
adduser -S -G wal-g -s /bin/bash wal-g && \
110+
adduser postgres wal-g
111+
112+
# Copy Nix store and profiles from builder (written to /nix-output to escape cache mount)
113+
COPY --from=nix-builder /nix-output /nix
114+
115+
# Copy groonga plugins
116+
COPY --from=nix-builder /tmp/groonga-plugins/plugins /usr/lib/groonga/plugins
117+
118+
# Copy gosu
119+
COPY --from=gosu-builder /usr/local/bin/gosu /usr/local/bin/gosu
120+
121+
# Setup PostgreSQL directories
122+
RUN mkdir -p /usr/lib/postgresql/bin \
123+
/usr/lib/postgresql/share/postgresql \
124+
/usr/share/postgresql \
125+
/var/lib/postgresql/data \
126+
/var/run/postgresql \
127+
&& chown -R postgres:postgres /usr/lib/postgresql \
128+
&& chown -R postgres:postgres /var/lib/postgresql \
129+
&& chown -R postgres:postgres /usr/share/postgresql \
130+
&& chown -R postgres:postgres /var/run/postgresql
131+
132+
# Create symbolic links for binaries
133+
RUN for f in /nix/var/nix/profiles/default/bin/*; do \
134+
ln -sf "$f" /usr/lib/postgresql/bin/ 2>/dev/null || true; \
135+
ln -sf "$f" /usr/bin/ 2>/dev/null || true; \
136+
done
137+
138+
# Create symbolic links for PostgreSQL shares
139+
RUN ln -sf /nix/var/nix/profiles/default/share/postgresql/* /usr/lib/postgresql/share/postgresql/ 2>/dev/null || true && \
140+
ln -sf /nix/var/nix/profiles/default/share/postgresql/* /usr/share/postgresql/ 2>/dev/null || true && \
141+
ln -sf /usr/lib/postgresql/share/postgresql/timezonesets /usr/share/postgresql/timezonesets 2>/dev/null || true
142+
143+
# Set permissions
144+
RUN chown -R postgres:postgres /usr/lib/postgresql && \
145+
chown -R postgres:postgres /usr/share/postgresql
146+
147+
# Setup configs
148+
COPY --chown=postgres:postgres ansible/files/postgresql_config/postgresql.conf.j2 /etc/postgresql/postgresql.conf
149+
COPY --chown=postgres:postgres ansible/files/postgresql_config/pg_hba.conf.j2 /etc/postgresql/pg_hba.conf
150+
COPY --chown=postgres:postgres ansible/files/postgresql_config/pg_ident.conf.j2 /etc/postgresql/pg_ident.conf
151+
COPY --chown=postgres:postgres ansible/files/postgresql_config/conf.d /etc/postgresql-custom/conf.d
152+
COPY --chown=postgres:postgres ansible/files/postgresql_config/postgresql-stdout-log.conf /etc/postgresql/logging.conf
153+
COPY --chown=postgres:postgres ansible/files/postgresql_config/supautils.conf.j2 /etc/postgresql-custom/supautils.conf
154+
COPY --chown=postgres:postgres ansible/files/postgresql_extension_custom_scripts /etc/postgresql-custom/extension-custom-scripts
155+
COPY --chown=postgres:postgres ansible/files/pgsodium_getkey_urandom.sh.j2 /usr/lib/postgresql/bin/pgsodium_getkey.sh
156+
COPY --chown=postgres:postgres ansible/files/postgresql_config/custom_walg.conf /etc/postgresql-custom/wal-g.conf
157+
COPY --chown=postgres:postgres ansible/files/postgresql_config/custom_read_replica.conf /etc/postgresql-custom/read-replica.conf
158+
COPY --chown=postgres:postgres ansible/files/walg_helper_scripts/wal_fetch.sh /home/postgres/wal_fetch.sh
159+
COPY ansible/files/walg_helper_scripts/wal_change_ownership.sh /root/wal_change_ownership.sh
160+
161+
# Configure PostgreSQL settings
162+
RUN sed -i \
163+
-e "s|#unix_socket_directories = '/tmp'|unix_socket_directories = '/var/run/postgresql'|g" \
164+
-e "s|#session_preload_libraries = ''|session_preload_libraries = 'supautils'|g" \
165+
-e "s|#include = '/etc/postgresql-custom/supautils.conf'|include = '/etc/postgresql-custom/supautils.conf'|g" \
166+
-e "s|#include = '/etc/postgresql-custom/wal-g.conf'|include = '/etc/postgresql-custom/wal-g.conf'|g" /etc/postgresql/postgresql.conf && \
167+
echo "pgsodium.getkey_script= '/usr/lib/postgresql/bin/pgsodium_getkey.sh'" >> /etc/postgresql/postgresql.conf && \
168+
echo "vault.getkey_script= '/usr/lib/postgresql/bin/pgsodium_getkey.sh'" >> /etc/postgresql/postgresql.conf && \
169+
chown -R postgres:postgres /etc/postgresql-custom
170+
171+
# Remove timescaledb and plv8 references (not in pg17)
172+
RUN sed -i 's/ timescaledb,//g;' "/etc/postgresql/postgresql.conf" && \
173+
sed -i 's/db_user_namespace = off/#db_user_namespace = off/g;' "/etc/postgresql/postgresql.conf" && \
174+
sed -i 's/ timescaledb,//g; s/ plv8,//g' "/etc/postgresql-custom/supautils.conf"
175+
176+
# Include schema migrations
177+
COPY migrations/db /docker-entrypoint-initdb.d/
178+
COPY ansible/files/pgbouncer_config/pgbouncer_auth_schema.sql /docker-entrypoint-initdb.d/init-scripts/00-schema.sql
179+
COPY ansible/files/stat_extension.sql /docker-entrypoint-initdb.d/migrations/00-extension.sql
180+
181+
# Add entrypoint script
182+
ADD --chmod=0755 \
183+
https://github.com/docker-library/postgres/raw/889f9447cd2dfe21cccfbe9bb7945e3b037e02d8/17/bullseye/docker-entrypoint.sh \
184+
/usr/local/bin/docker-entrypoint.sh
185+
186+
# Setup pgsodium key script
187+
RUN mkdir -p /usr/share/postgresql/extension/ && \
188+
ln -s /usr/lib/postgresql/bin/pgsodium_getkey.sh /usr/share/postgresql/extension/pgsodium_getkey && \
189+
chmod +x /usr/lib/postgresql/bin/pgsodium_getkey.sh
190+
191+
# Environment variables
192+
ENV PATH="/nix/var/nix/profiles/default/bin:/usr/lib/postgresql/bin:${PATH}"
193+
ENV PGDATA=/var/lib/postgresql/data
194+
ENV POSTGRES_HOST=/var/run/postgresql
195+
ENV POSTGRES_USER=supabase_admin
196+
ENV POSTGRES_DB=postgres
197+
ENV POSTGRES_INITDB_ARGS="--allow-group-access --locale-provider=icu --encoding=UTF-8 --icu-locale=en_US.UTF-8"
198+
ENV LANG=en_US.UTF-8
199+
ENV LANGUAGE=en_US:en
200+
ENV LC_ALL=en_US.UTF-8
201+
ENV GRN_PLUGINS_DIR=/usr/lib/groonga/plugins
202+
# Point to minimal glibc locales included in slim Nix package for initdb locale support
203+
ENV LOCALE_ARCHIVE=/nix/var/nix/profiles/default/lib/locale/locale-archive
204+
205+
ENTRYPOINT ["docker-entrypoint.sh"]
206+
HEALTHCHECK --interval=2s --timeout=2s --retries=10 CMD pg_isready -U postgres -h localhost
207+
STOPSIGNAL SIGINT
208+
EXPOSE 5432
209+
210+
CMD ["postgres", "-D", "/etc/postgresql"]

ansible/files/postgresql_config/postgresql.conf.j2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,7 @@ default_text_search_config = 'pg_catalog.english'
688688
#session_preload_libraries = ''
689689

690690
shared_preload_libraries = 'pg_stat_statements, pgaudit, plpgsql, plpgsql_check, pg_cron, pg_net, pgsodium, timescaledb, auto_explain, pg_tle, plan_filter, supabase_vault, pg_duckdb' # (change requires restart)
691+
duckdb.postgres_role = 'duckdb_role' # (change requires restart)
691692
jit_provider = 'llvmjit' # JIT library to use
692693

693694
# - Other Defaults -
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
-- migrate:up
2+
3+
-- Create a shared group role for DuckDB access.
4+
-- Both postgres (developer/admin) and service_role (runtime API) need to run
5+
-- DuckDB queries. We use a group role rather than cross-granting between them,
6+
-- which mirrors the supabase_privileged_role pattern.
7+
DO $$
8+
BEGIN
9+
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'duckdb_role') THEN
10+
CREATE ROLE duckdb_role;
11+
GRANT duckdb_role TO postgres WITH ADMIN OPTION;
12+
GRANT duckdb_role TO service_role, supabase_admin;
13+
END IF;
14+
END $$;
15+
16+
-- Event trigger for pg_duckdb
17+
-- Fires on CREATE EXTENSION pg_duckdb and grants FDW usage to duckdb_role.
18+
-- This mirrors the pattern used for pg_net and pg_cron.
19+
CREATE OR REPLACE FUNCTION extensions.grant_pg_duckdb_access()
20+
RETURNS event_trigger
21+
LANGUAGE plpgsql
22+
AS $$
23+
BEGIN
24+
IF EXISTS (
25+
SELECT 1
26+
FROM pg_event_trigger_ddl_commands() AS ev
27+
JOIN pg_extension AS ext
28+
ON ev.objid = ext.oid
29+
WHERE ext.extname = 'pg_duckdb'
30+
)
31+
THEN
32+
GRANT USAGE ON FOREIGN DATA WRAPPER duckdb TO duckdb_role;
33+
END IF;
34+
END;
35+
$$;
36+
37+
CREATE EVENT TRIGGER issue_pg_duckdb_access
38+
ON ddl_command_end
39+
WHEN TAG IN ('CREATE EXTENSION')
40+
EXECUTE PROCEDURE extensions.grant_pg_duckdb_access();
41+
42+
COMMENT ON FUNCTION extensions.grant_pg_duckdb_access IS 'Grants access to pg_duckdb';
43+
44+
-- Also apply immediately for existing installs where extension is already present
45+
DO $$
46+
BEGIN
47+
IF EXISTS (SELECT FROM pg_extension WHERE extname = 'pg_duckdb') THEN
48+
GRANT USAGE ON FOREIGN DATA WRAPPER duckdb TO duckdb_role;
49+
END IF;
50+
END $$;
51+
52+
-- migrate:down

nix/ext/pg_duckdb.nix

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ let
1717
inherit pname version;
1818

1919
src = fetchFromGitHub {
20-
owner = "duckdb";
20+
owner = "TylerHillery";
2121
repo = "pg_duckdb";
22-
rev = "v${version}";
23-
hash = "sha256-0cNfDZkd6x45xpWyPMfFoYAklE+4lAjO02SjV+V/dxU=";
22+
rev = "995fc34dd83659bed8f6ca3f2f66cc8eaa57fb14";
23+
hash = "sha256-QCDgXbeSEEu927zr1ctvI3UeH+xJiUFnY/SVwEeRyjk=";
2424
};
2525

2626
nativeBuildInputs = lib.optionals (!stdenv.isDarwin) [ patchelf ];
@@ -73,6 +73,8 @@ let
7373
printf '\n# Nix override: skip DuckDB cmake build\n$(FULL_DUCKDB_LIB):\n\t@:\n' >> Makefile
7474
'';
7575

76+
NIX_LDFLAGS = lib.optionalString stdenv.isDarwin "-headerpad_max_install_names";
77+
7678
makeFlags = [
7779
"PG_CONFIG=${postgresql}/bin/pg_config"
7880
];

0 commit comments

Comments
 (0)