Skip to content

Commit a9551ce

Browse files
committed
feat: add Dockerfile-supabase and Dockerfile-multigres as layered images
Introduces two new Dockerfiles: Dockerfile-supabase is a version-parameterised image (--build-arg PG_VERSION=17) built on Alpine + Nix, intended to replace the per-version Dockerfile-15/17 pattern. The pg17-specific config changes (stripping timescaledb, plv8, db_user_namespace) are wrapped in a version conditional so the same file works for pg15. conf.d is placed at /etc/postgresql/postgresql.conf.d with a backwards-compat symlink at /etc/postgresql-custom/conf.d. Dockerfile-multigres layers on top of Dockerfile-supabase and adds the Multigres-specific components: pgctld (built from Go source at a pinned commit) and pgbackrest (via apk). The pgctld wrapper script is a versioned file under docker/pgctld/ rather than an inline printf. wal-g files inherited from the base image are removed since Multigres uses pgbackrest for backups. pgctld runs as PID 1 and receives signals directly. The init SQL manifest generator is replaced with a --pg-initdb-sql-dir flag injected via the wrapper script (MUL-484, not yet implemented in pgctld). README.md is updated with build instructions for both images.
1 parent 6f02404 commit a9551ce

4 files changed

Lines changed: 307 additions & 299 deletions

File tree

Dockerfile-multigres

Lines changed: 50 additions & 299 deletions
Original file line numberDiff line numberDiff line change
@@ -1,319 +1,70 @@
1-
# syntax=docker/dockerfile:1.6
2-
# Multigres PostgreSQL 17 variants — vanilla and OrioleDB
1+
# syntax=docker/dockerfile:1.2
2+
# 1.2: minimum version for COPY --chmod
3+
# Multigres PostgreSQL image — layered on top of the supabase base image.
34
#
4-
# Build targets:
5-
# docker build -f Dockerfile-multigres --target variant-17 -t multigres-17 .
6-
# docker build -f Dockerfile-multigres --target variant-orioledb-17 -t multigres-orioledb-17 .
7-
8-
####################
9-
# Stage 0: Nix base — shared Alpine + Nix setup for all builders
10-
####################
11-
FROM alpine:3.23 AS nix-base
12-
13-
RUN apk add --no-cache \
14-
bash \
15-
coreutils \
16-
curl \
17-
shadow \
18-
sudo \
19-
xz
20-
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-
RUN cat <<EOF > /tmp/extra-nix.conf
27-
extra-experimental-features = nix-command flakes
28-
extra-substituters = https://nix-postgres-artifacts.s3.amazonaws.com
29-
extra-trusted-public-keys = nix-postgres-artifacts:dGZlQOvKcNEjvT7QEAJbcV6b6uk7VF/hWMjhYleiaLI=
30-
EOF
31-
RUN curl -L https://releases.nixos.org/nix/nix-2.34.6/install | sh -s -- --daemon --no-channel-add --yes --nix-extra-conf-file /tmp/extra-nix.conf
32-
ENV PATH="${PATH}:/nix/var/nix/profiles/default/bin"
33-
RUN nix --version
34-
35-
WORKDIR /nixpg
36-
COPY . .
37-
38-
####################
39-
# Stage 1a: Nix builder — PostgreSQL 17
40-
####################
41-
FROM nix-base AS nix-builder-17
42-
43-
RUN nix profile add path:.#psql_17_slim/bin path:.#pg-backrest path:.#pgctld
44-
45-
RUN nix store gc
46-
47-
RUN nix profile add path:.#supabase-groonga && \
48-
mkdir -p /tmp/groonga-plugins && \
49-
cp -r /nix/var/nix/profiles/default/lib/groonga/plugins /tmp/groonga-plugins/
50-
51-
RUN nix store gc
52-
53-
####################
54-
# Stage 1b: Nix builder — OrioleDB 17
55-
####################
56-
FROM nix-base AS nix-builder-orioledb-17
57-
58-
RUN nix profile add path:.#psql_orioledb-17_slim/bin path:.#pg-backrest path:.#pgctld
59-
60-
RUN nix store gc
61-
62-
RUN nix profile add path:.#supabase-groonga && \
63-
mkdir -p /tmp/groonga-plugins && \
64-
cp -r /nix/var/nix/profiles/default/lib/groonga/plugins /tmp/groonga-plugins/
65-
66-
RUN nix store gc
67-
68-
####################
69-
# Stage 2: Gosu builder
70-
####################
71-
FROM alpine:3.23 AS gosu-builder
72-
73-
ARG TARGETARCH
74-
ARG GOSU_VERSION=1.19
75-
ARG GO_VERSION=1.26.1
76-
77-
RUN apk add --no-cache curl git
78-
79-
# Install Go
80-
RUN curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${TARGETARCH}.tar.gz" | tar -C /usr/local -xz
81-
ENV PATH="/usr/local/go/bin:${PATH}"
82-
83-
# Build gosu from source
84-
RUN git clone --depth 1 --branch "${GOSU_VERSION}" https://github.com/tianon/gosu.git /gosu && \
85-
cd /gosu && \
86-
CGO_ENABLED=0 go build -ldflags="-s -w" -o /usr/local/bin/gosu . && \
87-
chmod +x /usr/local/bin/gosu
5+
# The supabase image must be built first:
6+
# docker build -f Dockerfile-supabase -t supabase-postgres:17 .
7+
# docker build -f Dockerfile-multigres -t multigres:17 .
8+
#
9+
# Target a different PostgreSQL version by pointing SUPABASE_IMAGE at the
10+
# appropriate supabase image (no PG_VERSION arg needed here):
11+
# docker build -f Dockerfile-multigres \
12+
# --build-arg SUPABASE_IMAGE=supabase-postgres:15 \
13+
# -t multigres:15 .
8814

8915
####################
90-
# Stage 3: Shared base — runtime Alpine + config + migrations
91-
# Both variants inherit from this stage. No /nix dependency here.
16+
# Stage 1: pgctld builder
9217
####################
93-
FROM alpine:3.23 AS base
94-
95-
RUN apk add --no-cache \
96-
bash \
97-
curl \
98-
openssh \
99-
procps \
100-
shadow \
101-
su-exec \
102-
tzdata \
103-
musl-locales \
104-
musl-locales-lang \
105-
&& rm -rf /var/cache/apk/*
106-
107-
RUN addgroup -S postgres && \
108-
adduser -S -G postgres -h /var/lib/postgresql -s /bin/bash postgres && \
109-
addgroup -S wal-g && \
110-
adduser -S -G wal-g -s /bin/bash wal-g && \
111-
adduser postgres wal-g
112-
113-
RUN mkdir -p \
114-
/usr/lib/postgresql/bin \
115-
/usr/lib/postgresql/share/postgresql \
116-
/usr/share/postgresql \
117-
/var/lib/postgresql/data \
118-
/var/run/postgresql \
119-
/etc/postgresql \
120-
/etc/postgresql-custom \
121-
&& chown -R postgres:postgres \
122-
/usr/lib/postgresql \
123-
/var/lib/postgresql \
124-
/usr/share/postgresql \
125-
/var/run/postgresql \
126-
/etc/postgresql \
127-
/etc/postgresql-custom
128-
129-
COPY --chown=postgres:postgres ansible/files/postgresql_config/postgresql.conf.j2 /etc/postgresql/postgresql.conf
130-
COPY --chown=postgres:postgres ansible/files/postgresql_config/pg_hba.conf.j2 /etc/postgresql/pg_hba.conf
131-
COPY --chown=postgres:postgres ansible/files/postgresql_config/pg_ident.conf.j2 /etc/postgresql/pg_ident.conf
132-
COPY --chown=postgres:postgres ansible/files/postgresql_config/conf.d /etc/postgresql-custom/conf.d
133-
COPY --chown=postgres:postgres ansible/files/postgresql_config/postgresql-stdout-log.conf /etc/postgresql/logging.conf
134-
COPY --chown=postgres:postgres ansible/files/postgresql_config/supautils.conf.j2 /etc/postgresql-custom/supautils.conf
135-
COPY --chown=postgres:postgres ansible/files/postgresql_extension_custom_scripts /etc/postgresql-custom/extension-custom-scripts
136-
COPY --chown=postgres:postgres ansible/files/postgresql_config/custom_walg.conf /etc/postgresql-custom/wal-g.conf
137-
COPY --chown=postgres:postgres ansible/files/postgresql_config/custom_read_replica.conf /etc/postgresql-custom/read-replica.conf
138-
COPY --chown=postgres:postgres ansible/files/pgsodium_getkey_urandom.sh.j2 /usr/lib/postgresql/bin/pgsodium_getkey.sh
18+
FROM golang:1.24-alpine AS pgctld-builder
13919

140-
# Apply settings shared by all variants:
141-
# - enable unix socket, session preload, config includes
142-
# - strip timescaledb and pgsodium (absent from all pg17 builds)
143-
# - comment out db_user_namespace (not supported in pg17)
144-
RUN sed -i \
145-
-e "s|#unix_socket_directories = '/tmp'|unix_socket_directories = '/var/run/postgresql'|g" \
146-
-e "s|#session_preload_libraries = ''|session_preload_libraries = 'supautils'|g" \
147-
-e "s|#include = '/etc/postgresql-custom/supautils.conf'|include = '/etc/postgresql-custom/supautils.conf'|g" \
148-
# skip wal-g - unused by multigres
149-
# -e "s|#include = '/etc/postgresql-custom/wal-g.conf'|include = '/etc/postgresql-custom/wal-g.conf'|g" \
150-
-e "s/ timescaledb,//g" \
151-
-e "s/ pgsodium,//g" \
152-
-e "s/db_user_namespace = off/#db_user_namespace = off/g" \
153-
# skip - managed by pgctld
154-
-e "s|^data_directory |#data_directory |g" \
155-
/etc/postgresql/postgresql.conf && \
156-
echo "pgsodium.getkey_script= '/usr/lib/postgresql/bin/pgsodium_getkey.sh'" >> /etc/postgresql/postgresql.conf && \
157-
echo "vault.getkey_script= '/usr/lib/postgresql/bin/pgsodium_getkey.sh'" >> /etc/postgresql/postgresql.conf && \
158-
chown -R postgres:postgres /etc/postgresql-custom && \
159-
mkdir -p /usr/share/postgresql/extension/ && \
160-
ln -s /usr/lib/postgresql/bin/pgsodium_getkey.sh /usr/share/postgresql/extension/pgsodium_getkey && \
161-
chmod +x /usr/lib/postgresql/bin/pgsodium_getkey.sh
20+
# Pinned to the commit recorded in flake.lock (multigres rev)
21+
ARG PGCTLD_REV=7c14506b93d62b86e7ba922a90dcbf7b5574aa09
16222

163-
COPY migrations/db /docker-entrypoint-initdb.d/
164-
COPY ansible/files/pgbouncer_config/pgbouncer_auth_schema.sql /docker-entrypoint-initdb.d/init-scripts/00-schema.sql
165-
COPY ansible/files/stat_extension.sql /docker-entrypoint-initdb.d/migrations/00-extension.sql
23+
RUN apk add --no-cache git
16624

167-
ENV PGDATA=/var/lib/postgresql/data
168-
ENV POSTGRES_HOST=/var/run/postgresql
169-
ENV POSTGRES_USER=supabase_admin
170-
ENV POSTGRES_DB=postgres
171-
ENV POSTGRES_INITDB_ARGS="--allow-group-access --locale-provider=icu --encoding=UTF-8 --icu-locale=en_US.UTF-8"
172-
ENV LANG=en_US.UTF-8
173-
ENV LANGUAGE=en_US:en
174-
ENV LC_ALL=en_US.UTF-8
175-
ENV GRN_PLUGINS_DIR=/usr/lib/groonga/plugins
25+
RUN git clone https://github.com/multigres/multigres.git /multigres && \
26+
cd /multigres && \
27+
git checkout ${PGCTLD_REV} && \
28+
# Copy pico CSS assets before build (mirrors pgctld.nix preBuild step)
29+
cp external/pico/pico.* go/common/web/templates/css/ 2>/dev/null || true && \
30+
CGO_ENABLED=0 go build -ldflags="-s -w" -o /usr/local/bin/pgctld ./go/cmd/pgctld
17631

17732
####################
178-
# Stage 4a: variant-17 — PostgreSQL 17 (vanilla)
33+
# Stage 2: Multigres image
17934
####################
180-
FROM base AS variant-17
35+
ARG SUPABASE_IMAGE=supabase-postgres:17
36+
FROM ${SUPABASE_IMAGE}
18137

182-
COPY --from=nix-builder-17 /nix /nix
183-
COPY --from=nix-builder-17 /tmp/groonga-plugins/plugins /usr/lib/groonga/plugins
184-
COPY --from=gosu-builder /usr/local/bin/gosu /usr/local/bin/gosu
38+
# Install pgbackrest (available in Alpine community repo)
39+
RUN apk add --no-cache pgbackrest
18540

186-
RUN for f in /nix/var/nix/profiles/default/bin/*; do \
187-
ln -sf "$f" /usr/lib/postgresql/bin/ 2>/dev/null || true; \
188-
ln -sf "$f" /usr/bin/ 2>/dev/null || true; \
189-
done && \
190-
ln -sf /nix/var/nix/profiles/default/share/postgresql/* /usr/lib/postgresql/share/postgresql/ 2>/dev/null || true && \
191-
ln -sf /nix/var/nix/profiles/default/share/postgresql/* /usr/share/postgresql/ 2>/dev/null || true && \
192-
ln -sf /usr/lib/postgresql/share/postgresql/timezonesets /usr/share/postgresql/timezonesets 2>/dev/null || true
41+
# Copy pgctld binary; keep it separate so the wrapper script can reference it cleanly
42+
COPY --from=pgctld-builder /usr/local/bin/pgctld /usr/local/bin/pgctld-bin
19343

194-
RUN chown -R postgres:postgres /usr/lib/postgresql && \
195-
chown -R postgres:postgres /usr/share/postgresql
196-
197-
# Can't use /etc/pgctld because it's a mount point
44+
# pgctld config template — /etc/pgctld is a mount point in k8s so use a custom dir
19845
COPY docker/pgctld/postgresql.conf.tmpl /etc/pgctld-custom/postgresql.conf.tmpl
19946

200-
# Wrapper: injects --postgres-config-template on every pgctld call so the team's
201-
# unmodified k8s manifests and local provisioner commands work without extra flags.
202-
RUN printf '#!/bin/sh\nexec /nix/var/nix/profiles/default/bin/pgctld --postgres-config-template /etc/pgctld-custom/postgresql.conf.tmpl "$@"\n' \
203-
> /usr/local/bin/pgctld && \
204-
chmod +x /usr/local/bin/pgctld
205-
206-
# Strip extensions absent from pg17 vanilla build
207-
RUN sed -i 's/ timescaledb,//g; s/ plv8,//g' /etc/postgresql-custom/supautils.conf
208-
209-
# Generate a single SQL manifest that pgctld runs via --init-db-sql-file after initdb.
210-
# Creates the postgres role, runs init-scripts as postgres (matching migrate.sh),
211-
# then runs migrations as supabase_admin.
212-
RUN set -e && \
213-
manifest=/docker-entrypoint-initdb.d/multigres-init.sql && \
214-
printf -- "-- Auto-generated: run init-scripts and migrations after initdb\n" > "$manifest" && \
215-
printf "DO \$\$ BEGIN\n IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'postgres') THEN\n CREATE ROLE postgres SUPERUSER LOGIN;\n END IF;\nEND \$\$;\n" >> "$manifest" && \
216-
printf "ALTER DATABASE postgres OWNER TO postgres;\n\n" >> "$manifest" && \
217-
printf "SET SESSION AUTHORIZATION postgres;\n" >> "$manifest" && \
218-
for f in $(ls /docker-entrypoint-initdb.d/init-scripts/*.sql 2>/dev/null | sort); do \
219-
printf '\\ir init-scripts/%s\n' "$(basename "$f")" >> "$manifest"; \
220-
done && \
221-
printf "\nRESET SESSION AUTHORIZATION;\n\n" >> "$manifest" && \
222-
for f in $(ls /docker-entrypoint-initdb.d/migrations/*.sql 2>/dev/null | sort); do \
223-
printf '\\ir migrations/%s\n' "$(basename "$f")" >> "$manifest"; \
224-
done && \
225-
chown postgres:postgres "$manifest"
226-
227-
ENV POSTGRES_INITDB_SQL_FILES=/docker-entrypoint-initdb.d/multigres-init.sql
228-
ENV PATH="/nix/var/nix/profiles/default/bin:/usr/lib/postgresql/bin:${PATH}"
229-
ENV LOCALE_ARCHIVE=/nix/var/nix/profiles/default/lib/locale/locale-archive
230-
231-
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
232-
CMD pg_isready -U postgres -h localhost || true
233-
STOPSIGNAL SIGINT
234-
USER postgres
235-
EXPOSE 5432
236-
237-
ENTRYPOINT ["tail"]
238-
CMD ["-f", "/dev/null"]
239-
240-
####################
241-
# Stage 4b: variant-orioledb-17 — PostgreSQL 17 with OrioleDB
242-
####################
243-
FROM base AS variant-orioledb-17
244-
245-
COPY --from=nix-builder-orioledb-17 /nix /nix
246-
COPY --from=nix-builder-orioledb-17 /tmp/groonga-plugins/plugins /usr/lib/groonga/plugins
247-
COPY --from=gosu-builder /usr/local/bin/gosu /usr/local/bin/gosu
248-
249-
RUN for f in /nix/var/nix/profiles/default/bin/*; do \
250-
ln -sf "$f" /usr/lib/postgresql/bin/ 2>/dev/null || true; \
251-
ln -sf "$f" /usr/bin/ 2>/dev/null || true; \
252-
done && \
253-
ln -sf /nix/var/nix/profiles/default/share/postgresql/* /usr/lib/postgresql/share/postgresql/ 2>/dev/null || true && \
254-
ln -sf /nix/var/nix/profiles/default/share/postgresql/* /usr/share/postgresql/ 2>/dev/null || true && \
255-
ln -sf /usr/lib/postgresql/share/postgresql/timezonesets /usr/share/postgresql/timezonesets 2>/dev/null || true
256-
257-
RUN chown -R postgres:postgres /usr/lib/postgresql && \
258-
chown -R postgres:postgres /usr/share/postgresql
259-
260-
# Strip extensions absent from orioledb-17 build
261-
RUN sed -i 's/ timescaledb,//g; s/ plv8,//g; s/ postgis,//g; s/ pgrouting,//g' \
262-
/etc/postgresql-custom/supautils.conf
263-
264-
# Add orioledb to shared_preload_libraries and configure as default table access method
265-
# Rewind: 1200s window, 100k transactions, 1280 x 8KB = 10MB buffer
266-
RUN sed -i "s/\(shared_preload_libraries.*\)'/\1, orioledb'/" /etc/postgresql/postgresql.conf && \
267-
echo "default_table_access_method = 'orioledb'" >> /etc/postgresql/postgresql.conf && \
268-
echo "orioledb.enable_rewind = true" >> /etc/postgresql/postgresql.conf && \
269-
echo "orioledb.rewind_max_time = 1200" >> /etc/postgresql/postgresql.conf && \
270-
echo "orioledb.rewind_max_transactions = 100000" >> /etc/postgresql/postgresql.conf && \
271-
echo "orioledb.rewind_buffers = 1280" >> /etc/postgresql/postgresql.conf
272-
273-
# Register orioledb before initdb migrations run
274-
RUN echo "CREATE EXTENSION orioledb;" > /docker-entrypoint-initdb.d/init-scripts/00-pre-init.sql && \
275-
chown postgres:postgres /docker-entrypoint-initdb.d/init-scripts/00-pre-init.sql
276-
277-
# pgctld calls initdb without locale flags, so it picks up LANG from the environment.
278-
# OrioleDB requires C, POSIX, or ICU collations; override the base stage's en_US.UTF-8.
279-
ENV LANG=C
280-
ENV LC_ALL=C
281-
ENV LANGUAGE=C
282-
283-
# Can't use /etc/pgctld because it's a mount point
284-
COPY docker/pgctld/orioledb-postgresql.conf.tmpl /etc/pgctld-custom/orioledb-postgresql.conf.tmpl
285-
286-
# Wrapper: injects --postgres-config-template on every pgctld call so the team's
287-
# unmodified k8s manifests and local provisioner commands work without extra flags.
288-
RUN printf '#!/bin/sh\nexec /nix/var/nix/profiles/default/bin/pgctld --postgres-config-template /etc/pgctld-custom/orioledb-postgresql.conf.tmpl "$@"\n' \
289-
> /usr/local/bin/pgctld && \
290-
chmod +x /usr/local/bin/pgctld
47+
# Wrapper: injects --postgres-config-template on every pgctld call so unmodified
48+
# k8s manifests and local provisioner commands work without extra flags
49+
COPY --chmod=0755 docker/pgctld/pgctld /usr/local/bin/pgctld
29150

292-
# Regenerate manifest after orioledb added 00-pre-init.sql to init-scripts/
293-
RUN set -e && \
294-
manifest=/docker-entrypoint-initdb.d/multigres-init.sql && \
295-
printf -- "-- Auto-generated: run init-scripts and migrations after initdb\n" > "$manifest" && \
296-
printf "DO \$\$ BEGIN\n IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'postgres') THEN\n CREATE ROLE postgres SUPERUSER LOGIN;\n END IF;\nEND \$\$;\n" >> "$manifest" && \
297-
printf "ALTER DATABASE postgres OWNER TO postgres;\n\n" >> "$manifest" && \
298-
printf "SET SESSION AUTHORIZATION postgres;\n" >> "$manifest" && \
299-
for f in $(ls /docker-entrypoint-initdb.d/init-scripts/*.sql 2>/dev/null | sort); do \
300-
printf '\\ir init-scripts/%s\n' "$(basename "$f")" >> "$manifest"; \
301-
done && \
302-
printf "\nRESET SESSION AUTHORIZATION;\n\n" >> "$manifest" && \
303-
for f in $(ls /docker-entrypoint-initdb.d/migrations/*.sql 2>/dev/null | sort); do \
304-
printf '\\ir migrations/%s\n' "$(basename "$f")" >> "$manifest"; \
305-
done && \
306-
chown postgres:postgres "$manifest"
51+
# /etc/postgresql/postgresql.conf is not modified here: pgctld renders its own
52+
# config from postgresql.conf.tmpl and passes it directly to PostgreSQL, so the
53+
# supabase base config (including wal-g and data_directory settings) is never loaded.
30754

308-
ENV POSTGRES_INITDB_SQL_FILES=/docker-entrypoint-initdb.d/multigres-init.sql
309-
ENV PATH="/nix/var/nix/profiles/default/bin:/usr/lib/postgresql/bin:${PATH}"
310-
ENV LOCALE_ARCHIVE=/nix/var/nix/profiles/default/lib/locale/locale-archive
55+
# wal-g is not used in Multigres (pgbackrest handles backups); remove inherited files.
56+
RUN rm -f \
57+
/etc/postgresql-custom/wal-g.conf \
58+
/home/postgres/wal_fetch.sh \
59+
/root/wal_change_ownership.sh
31160

312-
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
313-
CMD pg_isready -U postgres -h localhost || true
314-
STOPSIGNAL SIGINT
61+
# No HEALTHCHECK defined here: inherits the probe from the supabase base image.
62+
# Kubernetes ignores Docker HEALTHCHECK entirely — use readinessProbe in the Pod spec.
63+
# STOPSIGNAL inherited from supabase base image (SIGINT — smart shutdown).
31564
USER postgres
316-
EXPOSE 5432
31765

318-
ENTRYPOINT ["tail"]
319-
CMD ["-f", "/dev/null"]
66+
# pgctld is the cluster lifecycle manager for Multigres: it handles initdb,
67+
# config templating, replication setup, and coordinated restarts. Running it
68+
# as PID 1 ensures it receives stop signals directly and can shut down
69+
# PostgreSQL cleanly before the container exits.
70+
ENTRYPOINT ["/usr/local/bin/pgctld"]

0 commit comments

Comments
 (0)