Skip to content

feat: support newer Alpine-based Supabase Postgres images#58

Merged
andinux merged 10 commits into
mainfrom
feat/supabase-alpine-images
Jul 24, 2026
Merged

feat: support newer Alpine-based Supabase Postgres images#58
andinux merged 10 commits into
mainfrom
feat/supabase-alpine-images

Conversation

@damlayildiz

@damlayildiz damlayildiz commented Jul 22, 2026

Copy link
Copy Markdown
Member

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.084 and later). This PR makes the published
sqlitecloud/sqlite-sync-supabase images build cleanly on those newer bases, in
parallel 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 artifact
is the correct one
no musl build is required. Only the Dockerfile's build-time
userland tooling (apt-get vs apk) and the pg_config path differ between bases.

Changes

  • docker/postgresql/Dockerfile.supabase.release — made userland-agnostic:
    • detects apk vs apt-get to install the curl used for the download,
    • resolves pg_config dynamically (path differs: /usr/bin on Ubuntu vs the Nix
      default profile on Alpine),
    • installs the same glibc artifact on both; the NIX_PGLIBDIR install logic
      was already portable and is unchanged.
      One file now serves both base families.
  • .github/workflows/main.yml — docker-publish matrix:
    • added a PG17 Alpine row (base 17.6.1.151),
    • added a tag_suffix mechanism so Alpine publishes under distinct tags
      (:17-alpine, :17-alpine-<ver>, :17.6.1.151) with no collision against the
      :17 / :15 aliases,
    • bumped the PG15 base pin 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:
    • docs/postgresql/quickstarts/supabase-self-hosted.md — documented the
      Ubuntu-vs-Alpine base split, added the :17-alpine image option, updated the
      PG15 base reference to 15.8.1.135.
    • docker/README.md — note that the published release image handles both bases
      automatically, while the local build-from-source flow currently targets Ubuntu.
    • docs/internal/supabase-flyio.md — refreshed the illustrative PG15 base tag
      from 15.8.1.085 to 15.8.1.135 throughout.

Resulting published tags

Row Base image Published tags
PG17 Ubuntu (frozen line) 17.6.1.071 :17, :17-<ver>, :17.6.1.071
PG15 Ubuntu 15.8.1.135 :15, :15-<ver>, :15.8.1.135
PG17 Alpine 17.6.1.151 :17-alpine, :17-alpine-<ver>, :17.6.1.151

The shared :17 / :15 aliases 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)

  1. :17 / :15 alias scheme — deferred by design. Alpine is behind explicit
    -alpine + exact-base tags. Decide whether/when :17 should point at Alpine
    (matching what current Supabase self-hosting pulls), with the Ubuntu build moved
    to e.g. :17-ubuntu.
  2. PG17 Ubuntu row (17.6.1.071) — kept for back-compat. Supabase moved the PG17
    line 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 :17 alias in point 1 above.
  3. amd64 not tested locally — only arm64 was validated here. The glibc mechanism
    is arch-independent and CI builds linux/amd64,linux/arm64; the first CI run is
    the real amd64 proof.
  4. Local build-from-source (Dockerfile.supabase, make postgres-supabase-build)
    still targets Ubuntu bases (uses apt-get + hardcoded pg_config). Out of scope
    here, 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.
  5. Pin maintenance17.6.1.151 and 15.8.1.135 are 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

  • CI multi-arch build succeeds for all three Supabase rows (amd64 + arm64).
  • Pulled :17-alpine and :15 images load the extension (cloudsync_version()).
  • Existing :17 consumers unaffected — the PG17 Ubuntu row is unchanged at
    17.6.1.071, so the base image and its postgres UID (101:102) stay as they are.
  • Existing :15 consumers need a one-time chown — the 15.8.1.085 → 15.8.1.135
    bump crosses Ubuntu 20.04 → 24.04, which shifts the postgres UID 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:102 migration is verified on a live deployment. New
    deployments are unaffected.

Update — review follow-ups

Three commits on top of the original feature commit.

c4faf47 — review fixes

  • CI job name collision. The two PG17 rows both rendered as docker sqlite-sync-supabase pg17; the job name now includes tag_suffix.
  • apk del curl was a live regression. curl is already present in the real 17.6.1.151 Alpine base — apk add only had to install ca-certificates — so the previous unconditional apk del curl would 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 GNU tar install was dropped too; busybox tar extracts the release tarball.
  • CLOUDSYNC_PG_CONFIG is an ARG, not an ENV, so --build-arg actually overrides it (it previously could not) and the empty value no longer leaks into the published image.
  • Missing CLOUDSYNC_VERSION now fails with an actionable message instead of unbound variable.
  • pg_config resolution tries the Nix profiles the Supabase bases ship before falling back to PATH, so the Ubuntu images keep the exact path they resolved to before this PR.

b5c621e — doc nits

:17-alpine added 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 publish

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

Row smoke build smoke test push job total before
supabase pg17-alpine 47 s 2 s 37 s 118 s 92 s
supabase pg17 (Ubuntu) 151 s 3 s 71 s 264 s 122 s
supabase pg15 (Ubuntu) 126 s 3 s 55 s 223 s ~114–146 s
postgres pg17 19 s 3 s 132 s 173 s 175 s
postgres pg15 19 s 2 s 135 s 178 s 183 s

Workflow wall clock: 778 s vs 738 s (+40 s)docker-publish is 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 the load export, which scales with image size (hence the large Ubuntu Supabase rows).

Open points now settled

  • Point 3 (amd64 not tested) — resolved. The Alpine row's log shows CREATE EXTENSION1.1.2 on amd64, one second before the push begins. The real 17.6.1.151 base was additionally verified by hand on arm64 (full container boot + CREATE EXTENSION).
  • Point 4 (local build-from-source) — tracked in Local build-from-source (Dockerfile.supabase) assumes an Ubuntu Supabase base #60. Dockerfile.supabase still
    hardcodes apt-get and /root/.nix-profile/bin/pg_config, so it holds the opposite
    assumption to the now-portable Dockerfile.supabase.release in this PR. It is never
    built in CI, and only breaks once the Supabase CLI bundles an Alpine image.
  • Point 1 (:17 / :15 alias scheme) — stays deferred, now tracked in Ship :17-ubuntu before flipping the :17 Supabase alias to Alpine #59. Recommendation there: do not flip :17 until the Alpine image has been exercised inside a full Supabase stack, and ship :17-ubuntu one release ahead of the flip.

🤖 Generated with Claude Code

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
@damlayildiz
damlayildiz marked this pull request as draft July 22, 2026 16:01
@damlayildiz
damlayildiz requested a review from andinux July 22, 2026 17:03
@damlayildiz
damlayildiz marked this pull request as ready for review July 22, 2026 17:03
andinux and others added 3 commits July 23, 2026 10:51
- 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>
andinux and others added 2 commits July 23, 2026 18:44
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
@andinux

andinux commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Full-stack validation of :17-alpine, and a base-image UID finding for :15

Two results from testing this branch's images on real Fly.io deployments.

1. :17-alpine validated in a full Supabase stack ✅

Open point 3 (amd64 untested) and the full-stack question are both closed. sqlitecloud/sqlite-sync-supabase:17-alpine-beta-feat-supabase-alpine-images has been running on cloudsync-supabase-test-pg17 (amd64, Fly.io, shared-cpu-4x):

PostgreSQL 17.6 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 15.2.0, 64-bit
cloudsync_version() = 1.1.2
supabase-db ... Up 4 hours (healthy)

All 11 stack services healthy alongside it — Envoy, studio, storage, auth, realtime, rest, meta, supavisor, edge-functions, imgproxy. This is the case CI can't cover: other Supabase services connecting to the db container on an Alpine userland.

CI also now smoke-tests every published image before pushing it (CREATE EXTENSION cloudsync; SELECT cloudsync_version(); on a booted amd64 container), so :17-alpine was verified at build time as well.

2. The :15 base bump shifts the postgres UID

15.8.1.085 → 15.8.1.135 crosses an Ubuntu LTS boundary, which moves the postgres account's UID. Measured across the four relevant bases:

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 affected17.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 db

Verified 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".

andinux and others added 2 commits July 24, 2026 16:20
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>
@andinux
andinux merged commit 2646c12 into main Jul 24, 2026
26 of 27 checks passed
@andinux
andinux deleted the feat/supabase-alpine-images branch July 24, 2026 17:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants