feat: support newer Alpine-based Supabase Postgres images#58
Conversation
Make the published sqlite-sync-supabase images build on Supabase's newer Alpine-userland Postgres bases alongside the existing Ubuntu ones. Despite the Alpine userland these images run a glibc Nix postgres, so the same glibc extension artifact is installed - only the build-time tooling (apt-get vs apk) and the pg_config path differ. - Dockerfile.supabase.release: detect apk/apt-get and resolve pg_config dynamically; one file now serves both base families - main.yml: add PG17 Alpine publish row (17.6.1.151) with a -alpine tag suffix so it does not collide with the :17/:15 aliases; bump PG15 base pin 15.8.1.085 -> 15.8.1.135 - docs: document the Ubuntu vs Alpine base split and refresh illustrative PG15 base tags
- workflow: include tag_suffix in the docker-publish job name, so the Ubuntu and Alpine PG17 rows no longer render as the same check name - only remove curl again when it was not already in the base image, and skip installing it entirely when a beta artifact is staged; drop the GNU tar install (busybox tar extracts the release tarball) - CLOUDSYNC_PG_CONFIG is an ARG, so --build-arg actually overrides it and the empty value no longer leaks into the published image - fail with an actionable message when CLOUDSYNC_VERSION is not passed - resolve pg_config from the Nix profiles the Supabase bases ship before falling back to PATH, keeping the Ubuntu images on their previous path Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Also drop the pinned CloudSync version from the Dockerfile usage example so it does not go stale. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Build amd64 natively and load it, then boot the image and run CREATE EXTENSION cloudsync / cloudsync_version() against it. The multi-arch push only runs once that passes, so a broken artifact cannot reach Docker Hub, and amd64 stops being the untested arch. Readiness is probed over TCP because the entrypoint's init-time server listens on the unix socket only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Single-Machine Docker-in-Docker image: dockerd plus a pinned copy of the upstream Supabase compose tree, with our overrides layered via COMPOSE_FILE and all state on a Fly volume at /data. No deployment-specific identifier is committed: the app name, region and org are passed to `fly deploy`, the public URLs are derived from FLY_APP_NAME at boot, and the image tag defaults to the released :17-alpine with CLOUDSYNC_IMAGE as the override for beta builds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- layer docker-compose.envoy.yml so the stack runs Envoy (api-gw) instead of Kong, ahead of upstream making it the default in the ~Aug 2026 release - keep a failed `compose up` from crash-looping the Machine into Fly's restart cap, and bound the Postgres wait so post-init cannot hang - remove containers whose service left the compose config before starting: --remove-orphans ignores profile-disabled services, and the stale supabase-kong container held port 8000 so Envoy could not bind - document the deployment on its own and drop the superseded manual VM guide
Full-stack validation of
|
| base | OS | postgres UID:GID |
|---|---|---|
15.8.1.085 (current :15) |
Ubuntu 20.04.6 | 105:106 |
15.8.1.135 (this PR's :15) |
Ubuntu 24.04.3 | 101:102 |
17.6.1.071 (current :17) |
Ubuntu 24.04.3 | 101:102 |
17.6.1.151 (:17-alpine) |
Alpine 3.23 | 100:101 |
postgresql-common creates the account with adduser --system, which takes the lowest free UID in the 100–999 range. Nobody pins it, so the value depends on how many system users the base image created first. Ubuntu 24.04 moved _apt to 42 and pushed the systemd-* users to descend from 999, vacating 101–104.
:15 is the only tag affected — 17.6.1.071 is already on 24.04, so the PG17 Ubuntu line doesn't move.
Reproduced on an existing :15 deployment (data directory created by 15.8.1.085, all 2156 files owned by 105:106). Starting the new image gives:
cat: /etc/postgresql-custom/pgsodium_root.key: Permission denied
FATAL: invalid secret key
The key file is mode 0600 owned by 105:106, so uid 101 gets EACCES. Past that, PostgreSQL also refuses a data directory it doesn't own.
The fix is a one-line chown on the two postgres-owned paths, with the container stopped:
docker compose stop db
chown -R 101:102 ./volumes/db/data # PGDATA
chown -R 101:102 /var/lib/docker/volumes/supabase_db-config/_data # /etc/postgresql-custom
docker compose up -d dbVerified on a real deployment. Applied to cloudsync-supabase-test (existing :15-1.0.20 install, ~2 months of data, 2156 files re-owned), then started this PR's 15.8.1.135-based image:
chown -R 101:102 → 2156 files at 101:102
pg_ready_after=1
PostgreSQL 15.8 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 13.2.0, 64-bit
cloudsync_version() = 1.1.2
ALTER EXTENSION cloudsync UPDATE; → extversion 1.1
supabase-db ... healthy
Postgres came up on the first readiness probe, the 1.1.2 binary loaded against the existing catalog, and the 1.0 → 1.1 extension upgrade applied cleanly. No data loss, no reinitialisation.
Fresh deployments are unaffected — this only applies to volumes initialised by the older base.
Worth adding to the quickstart's upgrade notes: if a :15 update fails with pgsodium_root.key: Permission denied / FATAL: invalid secret key, it's the UID shift, and chown -R 101:102 on those two paths resolves it. Note the chown is one-way — after it, the old 15.8.1.085-based image can no longer read the data directory — so take a pg_dumpall first.
One doc wording note
docs/postgresql/quickstarts/supabase-self-hosted.md says of the Ubuntu and Alpine images: "The extension itself is identical on both ... so functionally they are interchangeable." That's true of the extension, but the postgres UIDs differ (101:102 vs 100:101), so switching an existing deployment between :17 and :17-alpine hits the same chown requirement. Worth qualifying as "interchangeable for new deployments".
Supabase base images do not pin the postgres user's UID (105 on the Ubuntu 20.04 based 15.8.1.085, 101 on the Ubuntu 24.04 based 15.8.1.135, 100 on the Alpine based 17.6.1.151), so a data directory created under one base is unreadable by another and Postgres fails with "pgsodium_root.key: Permission denied" / "invalid secret key". Document the chown migration, and qualify the Ubuntu-vs-Alpine note: the two are interchangeable for new deployments, not for existing ones. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Verified against a live roundtrip against the deployed stack: - registration needs a workspaceId for org-wide keys, plus jwtSecret and the issuer/audience: this GoTrue is HS256-only and serves an empty JWKS - there is no /v1/databases/:id/verify endpoint; cloudsync/tables is the connectivity check - the SQLite client must declare INTEGER where Postgres has boolean, since the schema hash normalizes PG boolean to integer and SQLite BOOLEAN to text - cloudsync_uuid() in a DEFAULT clause needs an explicit id on insert, and a non-default server needs cloudsync_network_init_custom - read the token issuer from GOTRUE_JWT_ISSUER, not from .env - keep server hostnames out of the doc Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The :15 bump moves the base from Ubuntu 20.04 to 24.04, which shifts the postgres UID and makes an existing data directory unreadable until it is chowned. CHANGELOG.md is the only channel that reaches users who pull the image, since changelog-action publishes it to the website on release. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Support newer Alpine-based Supabase Postgres images
Summary
Supabase's Postgres images switched their userland from Ubuntu to Alpine for the
current PG17 line (~
17.6.1.084and later). This PR makes the publishedsqlitecloud/sqlite-sync-supabaseimages build cleanly on those newer bases, inparallel with the existing Ubuntu-based images, and refreshes the pinned base tags.
Key finding: despite the Alpine userland, the PostgreSQL that actually runs on
these images is still a Nix-built, glibc-linked binary (every shipped plugin
needs
libc.so.6+ a nix-glibc RUNPATH). So the existing glibc extension artifactis the correct one — no musl build is required. Only the Dockerfile's build-time
userland tooling (
apt-getvsapk) and thepg_configpath differ between bases.Changes
docker/postgresql/Dockerfile.supabase.release— made userland-agnostic:apkvsapt-getto install thecurlused for the download,pg_configdynamically (path differs:/usr/binon Ubuntu vs the Nixdefault profile on Alpine),
NIX_PGLIBDIRinstall logicwas already portable and is unchanged.
One file now serves both base families.
.github/workflows/main.yml— docker-publish matrix:17.6.1.151),tag_suffixmechanism so Alpine publishes under distinct tags(
:17-alpine,:17-alpine-<ver>,:17.6.1.151) with no collision against the:17/:15aliases,15.8.1.085 → 15.8.1.135(latest published 15 build;still Ubuntu — Supabase kept the 15 line on Ubuntu, 20.04→24.04).
docs/postgresql/quickstarts/supabase-self-hosted.md— documented theUbuntu-vs-Alpine base split, added the
:17-alpineimage option, updated thePG15 base reference to
15.8.1.135.docker/README.md— note that the published release image handles both basesautomatically, while the local build-from-source flow currently targets Ubuntu.
docs/internal/supabase-flyio.md— refreshed the illustrative PG15 base tagfrom
15.8.1.085to15.8.1.135throughout.Resulting published tags
17.6.1.071:17,:17-<ver>,:17.6.1.07115.8.1.135:15,:15-<ver>,:15.8.1.13517.6.1.151:17-alpine,:17-alpine-<ver>,:17.6.1.151The shared
:17/:15aliases intentionally stay on the Ubuntu builds for now(non-breaking for existing pins).
Verification (local, arm64)
Built each image with its staged glibc artifact and ran
CREATE EXTENSION cloudsync; SELECT cloudsync_version();→1.1.2:17.6.1.071(Ubuntu / apt branch) — ✅15.8.1.135(Ubuntu 24.04 / apt branch) — ✅17.6.1.151(Alpine / apk branch) — ✅Open points (please check)
:17/:15alias scheme — deferred by design. Alpine is behind explicit-alpine+ exact-base tags. Decide whether/when:17should point at Alpine(matching what current Supabase self-hosting pulls), with the Ubuntu build moved
to e.g.
:17-ubuntu.17.6.1.071) — kept for back-compat. Supabase moved the PG17line to Alpine, so this row is frozen on an old Ubuntu base (nothing newer to
bump to) but still builds and works. Decision: keep it; the only related question
is the
:17alias in point 1 above.is arch-independent and CI builds
linux/amd64,linux/arm64; the first CI run isthe real amd64 proof.
Dockerfile.supabase,make postgres-supabase-build)still targets Ubuntu bases (uses
apt-get+ hardcodedpg_config). Out of scopehere, but if the Supabase CLI bundles an Alpine image, this breaks. Compile-in-image
on Alpine was proven to work — easy follow-up if wanted.
17.6.1.151and15.8.1.135are exact pins and will age;bumping is a manual edit (same as before). Optionally we could resolve the latest
base tag dynamically in CI, at the cost of build reproducibility. Recommend keeping
pins.
Test plan
:17-alpineand:15images load the extension (cloudsync_version()).:17consumers unaffected — the PG17 Ubuntu row is unchanged at17.6.1.071, so the base image and itspostgresUID (101:102) stay as they are.:15consumers need a one-timechown— the15.8.1.085 → 15.8.1.135bump crosses Ubuntu 20.04 → 24.04, which shifts the
postgresUID 105:106 →101:102 and makes an existing data directory unreadable (
pgsodium_root.key: Permission denied/FATAL: invalid secret key). Documented in the quickstart;the
chown -R 101:102migration is verified on a live deployment. Newdeployments are unaffected.
Update — review follow-ups
Three commits on top of the original feature commit.
c4faf47— review fixesdocker sqlite-sync-supabase pg17; the job name now includestag_suffix.apk del curlwas a live regression. curl is already present in the real17.6.1.151Alpine base —apk addonly had to installca-certificates— so the previous unconditionalapk del curlwould have stripped curl out of every published Alpine image. It is now removed only if it was not already in the base, and is not installed at all when a beta artifact is staged. The GNUtarinstall was dropped too; busybox tar extracts the release tarball.CLOUDSYNC_PG_CONFIGis anARG, not anENV, so--build-argactually overrides it (it previously could not) and the empty value no longer leaks into the published image.CLOUDSYNC_VERSIONnow fails with an actionable message instead ofunbound variable.pg_configresolution tries the Nix profiles the Supabase bases ship before falling back toPATH, so the Ubuntu images keep the exact path they resolved to before this PR.b5c621e— doc nits:17-alpineadded to the quickstart's "For Existing Deployments" block and to the release-notes body; the pinned CloudSync version in the Dockerfile usage example replaced with a placeholder.1a3e635— CI smoke test before publishEach matrix row now builds amd64 natively, loads it, boots it and runs
CREATE EXTENSION cloudsync; SELECT cloudsync_version();before the multi-arch push, so a broken artifact cannot reach Docker Hub.Measured cost on run 29998811353 — all five rows green:
Workflow wall clock: 778 s vs 738 s (+40 s) —
docker-publishis not on the critical path. The push step dropped 17–22 s on every row, confirming the amd64 layers are reused from the builder cache; the remaining cost is theloadexport, which scales with image size (hence the large Ubuntu Supabase rows).Open points now settled
CREATE EXTENSION→1.1.2on amd64, one second before the push begins. The real17.6.1.151base was additionally verified by hand on arm64 (full container boot +CREATE EXTENSION).Dockerfile.supabasestillhardcodes
apt-getand/root/.nix-profile/bin/pg_config, so it holds the oppositeassumption to the now-portable
Dockerfile.supabase.releasein this PR. It is neverbuilt in CI, and only breaks once the Supabase CLI bundles an Alpine image.
:17/:15alias scheme) — stays deferred, now tracked in Ship :17-ubuntu before flipping the :17 Supabase alias to Alpine #59. Recommendation there: do not flip:17until the Alpine image has been exercised inside a full Supabase stack, and ship:17-ubuntuone release ahead of the flip.🤖 Generated with Claude Code