Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 63 additions & 5 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@ jobs:
[**Android**](https://central.sonatype.com/artifact/ai.sqlite/sync): `ai.sqlite:sync:${{ steps.tag.outputs.version }}`
[**Swift**](https://github.com/sqliteai/sqlite-sync#swift-package): [Installation Guide](https://github.com/sqliteai/sqlite-sync#swift-package)
[**Docker (PostgreSQL)**](https://hub.docker.com/r/sqlitecloud/sqlite-sync-postgres): `docker pull sqlitecloud/sqlite-sync-postgres:17` or `:15`
[**Docker (Supabase)**](https://hub.docker.com/r/sqlitecloud/sqlite-sync-supabase): `docker pull sqlitecloud/sqlite-sync-supabase:17` or `:15`
[**Docker (Supabase)**](https://hub.docker.com/r/sqlitecloud/sqlite-sync-supabase): `docker pull sqlitecloud/sqlite-sync-supabase:17`, `:15` or `:17-alpine`

---

Expand All @@ -631,7 +631,7 @@ jobs:

docker-publish:
runs-on: ubuntu-22.04
name: docker ${{ matrix.image }} pg${{ matrix.pg_major }}${{ github.ref != 'refs/heads/main' && ' beta' || '' }}
name: docker ${{ matrix.image }} pg${{ matrix.pg_major }}${{ matrix.tag_suffix }}${{ github.ref != 'refs/heads/main' && ' beta' || '' }}
needs: [build, postgres-test, postgres-build, release]
# main: publish the versioned release tags, only when the release job cut a new
# release. Any other branch: release is skipped, publish/overwrite a per-branch
Expand Down Expand Up @@ -659,7 +659,16 @@ jobs:
- image: sqlite-sync-supabase
pg_major: '15'
dockerfile: docker/postgresql/Dockerfile.supabase.release
supabase_tag: '15.8.1.085'
supabase_tag: '15.8.1.135'
# Newer Supabase 17 images use an Alpine userland (still a glibc Nix
# postgres, so the same glibc artifact is installed). Published behind
# explicit -alpine / exact-base tags; the shared :17 alias stays on the
# Ubuntu build until we decide the alias scheme.
- image: sqlite-sync-supabase
pg_major: '17'
dockerfile: docker/postgresql/Dockerfile.supabase.release
supabase_tag: '17.6.1.151'
tag_suffix: '-alpine'

steps:

Expand Down Expand Up @@ -729,19 +738,68 @@ jobs:
VERSION=${{ steps.version.outputs.version }}
IMAGE=${{ env.DOCKERHUB_ORG }}/${{ matrix.image }}
SUPABASE_TAG=${{ matrix.supabase_tag }}
# Empty for the default (Ubuntu) rows; "-alpine" for the Alpine variant.
# Keeps the Alpine images from colliding with the :<major> aliases.
SUFFIX="${{ matrix.tag_suffix }}"
BETA="${{ steps.beta.outputs.slug }}"
{
if [ -n "$BETA" ]; then
echo "tags=${IMAGE}:${{ matrix.pg_major }}-beta-${BETA}"
echo "tags=${IMAGE}:${{ matrix.pg_major }}${SUFFIX}-beta-${BETA}"
else
echo "tags=${IMAGE}:${{ matrix.pg_major }},${IMAGE}:${{ matrix.pg_major }}-${VERSION},${IMAGE}:${SUPABASE_TAG}"
echo "tags=${IMAGE}:${{ matrix.pg_major }}${SUFFIX},${IMAGE}:${{ matrix.pg_major }}${SUFFIX}-${VERSION},${IMAGE}:${SUPABASE_TAG}"
fi
echo "build_args<<EOF"
echo "SUPABASE_POSTGRES_TAG=${SUPABASE_TAG}"
echo "CLOUDSYNC_VERSION=${VERSION}"
echo "EOF"
} >> $GITHUB_OUTPUT

# Build amd64 natively first and load it into the daemon, so the extension
# is proven to load before anything is published. The multi-arch push below
# reuses these layers from the builder cache.
- name: build amd64 image for the smoke test
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: linux/amd64
load: true
tags: cloudsync-smoke:test
build-args: ${{ matrix.image == 'sqlite-sync-postgres' && steps.standalone.outputs.build_args || steps.supabase.outputs.build_args }}

- name: smoke test the extension
env:
# the supabase bases ship a `postgres` role that cannot create the
# extension; their own docs use supabase_admin
PSQL_USER: ${{ matrix.image == 'sqlite-sync-supabase' && 'supabase_admin' || 'postgres' }}
run: |
docker run -d --name cloudsync-smoke -e POSTGRES_PASSWORD=postgres cloudsync-smoke:test
# the entrypoint's init-time server listens on the unix socket only, so
# a TCP connection only succeeds once the real server is up
ready=''
for _ in $(seq 1 60); do
if docker exec -e PGPASSWORD=postgres cloudsync-smoke \
psql -h 127.0.0.1 -U "$PSQL_USER" -d postgres -tAc 'SELECT 1' >/dev/null 2>&1; then
ready=1
break
fi
sleep 2
done
if [ -z "$ready" ]; then
echo "::error::postgres never accepted TCP connections"
docker logs cloudsync-smoke
exit 1
fi
if ! docker exec -e PGPASSWORD=postgres cloudsync-smoke \
psql -h 127.0.0.1 -U "$PSQL_USER" -d postgres -v ON_ERROR_STOP=1 \
-c 'CREATE EXTENSION IF NOT EXISTS cloudsync;' \
-c 'SELECT cloudsync_version();'; then
echo "::error::cloudsync failed to load in ${{ matrix.image }} pg${{ matrix.pg_major }}${{ matrix.tag_suffix }}"
docker logs cloudsync-smoke
exit 1
fi
docker rm -f cloudsync-smoke

- uses: docker/build-push-action@v6
with:
context: .
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ openssl/
unittest
.env

# Generated Supabase secrets, if a deployment .env is ever pulled down locally
/deploy/supabase-flyio/.env*

# Node.js
**/node_modules/**
package-lock.json
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

### Added

- **`sqlitecloud/sqlite-sync-supabase:17-alpine`** — CloudSync on the newer Alpine-userland Supabase Postgres images (roughly `17.6.1.084` and later). The extension is identical to the Ubuntu build — the PostgreSQL binary is glibc-linked on both — so only the base image differs. Use it when your Supabase stack pins one of these newer bases; `:17` continues to track the Ubuntu base.

### Changed

- **The `sqlitecloud/sqlite-sync-supabase:15` image now builds on Supabase base `15.8.1.135`** (previously `15.8.1.085`), which moves it from Ubuntu 20.04 to 24.04. Ubuntu 24.04 allocates system UIDs differently, so the `postgres` user changes from `105:106` to `101:102` and an **existing** data directory becomes unreadable: the container fails with `cat: /etc/postgresql-custom/pgsodium_root.key: Permission denied` followed by `FATAL: invalid secret key`. Existing deployments need a one-time `chown -R 101:102` on the data directory and the `supabase_db-config` volume before starting the new image — see [Self-Hosted Supabase](docs/postgresql/quickstarts/supabase-self-hosted.md) for the procedure. New deployments are unaffected, and the CloudSync extension itself is unchanged.

## [1.1.2] - 2026-07-13

### Fixed
Expand Down
23 changes: 23 additions & 0 deletions deploy/supabase-flyio/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Self-hosted Supabase + CloudSync, packaged as a single Fly Machine image.
#
# The image ships dockerd + docker compose and a pinned copy of the upstream
# Supabase `docker/` directory. Everything the stack needs at runtime (images,
# PGDATA, .env) lives on the Fly volume mounted at /data, so machine restarts
# and redeploys keep the data and re-run the same bootstrap.
FROM docker:28-dind

# fuse-overlayfs: Docker's overlay2 driver fails inside a Fly Machine
# ("failed to convert whiteout file: operation not permitted").
RUN apk add --no-cache bash fuse-overlayfs openssl

# Pinned upstream Supabase docker/ tree. Bump to adopt new service versions.
ARG SUPABASE_REF=312d05af4be0a404325417254d57f69378a0091c
RUN wget -qO /tmp/sb.tgz "https://codeload.github.com/supabase/supabase/tar.gz/${SUPABASE_REF}" \
&& tar -xzf /tmp/sb.tgz -C /tmp \
&& mv /tmp/supabase-*/docker /supabase \
&& rm -rf /tmp/sb.tgz /tmp/supabase-*

COPY docker-compose.cloudsync.yml cloudsync.sql entrypoint.sh /supabase/
RUN chmod +x /supabase/entrypoint.sh

ENTRYPOINT ["/supabase/entrypoint.sh"]
1 change: 1 addition & 0 deletions deploy/supabase-flyio/cloudsync.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE EXTENSION IF NOT EXISTS cloudsync;
17 changes: 17 additions & 0 deletions deploy/supabase-flyio/docker-compose.cloudsync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Layered on top of the upstream docker-compose.yml via COMPOSE_FILE.
# Keeps the upstream file untouched so it can be re-pinned without merge work.
services:
db:
# Released tag by default; set CLOUDSYNC_IMAGE (e.g. `fly secrets set`) to
# run a beta build without touching the repo.
image: ${CLOUDSYNC_IMAGE:-sqlitecloud/sqlite-sync-supabase:17-alpine}
volumes:
- ./cloudsync.sql:/docker-entrypoint-initdb.d/init-scripts/100-cloudsync.sql:Z
ports:
# Direct Postgres, bypassing Supavisor (which already owns 5432).
- "5433:5432"

supavisor:
# Skips /app/limits.sh, whose `ulimit -n 100000` is refused by the Fly kernel
# and crashes the container. Same tini init, same command.
entrypoint: ["/usr/bin/tini", "-s", "-g", "--"]
103 changes: 103 additions & 0 deletions deploy/supabase-flyio/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/bin/sh
# Boots the whole stack. Idempotent: runs on every machine start, including
# after `fly deploy`, `fly machine restart` and crashes.
set -eu

DATA_DIR=/data/supabase-docker
DOCKERD_LOG=/data/dockerd.log
INIT_MARKER=/data/.cloudsync-post-init-done

echo "=== configuring dockerd ==="
mkdir -p /etc/docker /data/docker
cat > /etc/docker/daemon.json <<'JSON'
{
"storage-driver": "fuse-overlayfs",
"data-root": "/data/docker",
"ipv6": true,
"ip6tables": true,
"fixed-cidr-v6": "fd00:d0c::/64"
}
JSON

echo "=== starting dockerd ==="
dockerd >>"$DOCKERD_LOG" 2>&1 &
until docker info >/dev/null 2>&1; do sleep 1; done

echo "=== syncing compose tree to $DATA_DIR ==="
mkdir -p "$DATA_DIR"
# Config comes from the image on every boot; state (.env, volumes/) stays.
cp -R /supabase/. "$DATA_DIR/"
cd "$DATA_DIR"

set_env() {
grep -q "^$1=" .env && sed -i "s|^$1=.*|$1=$2|" .env || echo "$1=$2" >> .env
}

if [ ! -f .env ]; then
echo "=== first boot: generating secrets ==="
cp .env.example .env
sh ./utils/generate-keys.sh --update-env
sh ./utils/add-new-auth-keys.sh --update-env
set_env POSTGRES_PASSWORD "$(openssl rand -hex 24)"
set_env DASHBOARD_PASSWORD "$(openssl rand -hex 12)"
fi

# Public hostnames follow from the app name, which Fly injects, so no
# deployment-specific URL has to live in the repo. Fly secrets still win.
if [ -n "${FLY_APP_NAME:-}" ]; then
: "${SUPABASE_PUBLIC_URL:=https://$FLY_APP_NAME.fly.dev}"
: "${API_EXTERNAL_URL:=$SUPABASE_PUBLIC_URL/auth/v1}"
export SUPABASE_PUBLIC_URL API_EXTERNAL_URL
fi

# Services dropped from the active config — a gateway swapped out via an override
# profile, say — keep their containers and their published ports, which blocks the
# replacement from binding. `--remove-orphans` does not cover them.
active_services=$(docker compose config --services 2>/dev/null || true)
docker compose ps -a --format '{{.Service}}|{{.Name}}' 2>/dev/null | while IFS='|' read -r svc name; do
[ -n "$svc" ] || continue
echo "$active_services" | grep -qx "$svc" && continue
echo "=== removing disabled service: $svc ($name) ==="
docker rm -f "$name" >/dev/null 2>&1 || true
done

echo "=== starting supabase ==="
# A service that fails its dependency conditions must not kill the machine:
# exiting here would crash-loop until Fly gives up, taking the healthy services
# with it and leaving nothing to debug.
docker compose up -d --remove-orphans || echo "WARNING: compose up reported a failure, continuing"

echo "=== waiting for postgres ==="
pg_ready=0
for _ in $(seq 1 90); do
if docker compose exec -T db pg_isready -U postgres -h localhost >/dev/null 2>&1; then
pg_ready=1
break
fi
sleep 2
done

if [ "$pg_ready" = 1 ]; then
# Init scripts only run on an empty PGDATA, so create the extension explicitly.
docker compose exec -T db psql -U postgres -c "CREATE EXTENSION IF NOT EXISTS cloudsync;" || true
else
echo "WARNING: postgres never became ready, skipping post-init"
fi

if [ "$pg_ready" = 1 ] && [ ! -f "$INIT_MARKER" ]; then
# GoTrue runs its migrations as supabase_auth_admin and must own these.
echo "=== fixing auth function ownership ==="
for fn in "auth.uid()" "auth.role()" "auth.email()"; do
docker compose exec -T db psql -U postgres \
-c "ALTER FUNCTION $fn OWNER TO supabase_auth_admin;" || true
done
docker compose restart auth
touch "$INIT_MARKER"
fi

echo "=== supabase is up ==="
docker compose ps

# Stream service logs to `fly logs`. If this exits the machine restarts and the
# whole bootstrap re-runs.
exec docker compose logs --no-color -f
43 changes: 43 additions & 0 deletions deploy/supabase-flyio/fly.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# No `app` / `primary_region` here on purpose: both are deployment-specific and
# are passed on the command line instead.
#
# fly deploy . --config fly.toml --app "$FLY_APP" --primary-region "$FLY_REGION"
#
# The public URLs follow from FLY_APP_NAME at boot (see entrypoint.sh) and the
# CloudSync image tag from CLOUDSYNC_IMAGE (see docker-compose.cloudsync.yml);
# override either with `fly secrets set`.

[build]
dockerfile = "Dockerfile"

[env]
# Envoy replaces Kong as the API gateway. Upstream makes this the default in
# the ~Aug 2026 release, after which the override becomes a no-op shim.
COMPOSE_FILE = "docker-compose.yml:docker-compose.envoy.yml:docker-compose.cloudsync.yml"
SITE_URL = "http://localhost:3000"
POOLER_TENANT_ID = "cloudsync"
STUDIO_DEFAULT_ORGANIZATION = "CloudSync"
STUDIO_DEFAULT_PROJECT = "Supabase"

[[mounts]]
source = "supabase_data"
destination = "/data"
initial_size = "50gb"

[[vm]]
size = "shared-cpu-4x"
memory = "4096"

# API gateway. Gives Studio and the REST/Auth/Storage APIs HTTPS on <app>.fly.dev.
[http_service]
internal_port = 8000
force_https = true
auto_stop_machines = "off"
auto_start_machines = false
min_machines_running = 1

# Postgres (container port 5433) is intentionally NOT published. Same-org apps
# reach it at <app>.internal:5433; laptops use `fly proxy 5433`. To expose it
# publicly, add a [[services]] block with internal_port = 5433 — note that a
# shared IPv4 serves 80/443 only and Postgres cannot use Fly's TLS handler, so
# public IPv4 access needs a dedicated IPv4 (`fly ips allocate-v4`).
9 changes: 9 additions & 0 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@ includes CloudSync, then tag it with the same name so the CLI reuses it. This
keeps your local Supabase stack intact (auth, realtime, storage, etc.) while
enabling the extension in the CLI-managed Postgres container.

> **Note on Ubuntu vs Alpine bases.** Newer Supabase Postgres images (roughly
> `17.6.1.084` and later) use an Alpine-based userland instead of the earlier
> Ubuntu one. The published release image (`Dockerfile.supabase.release`, used
> for `sqlitecloud/sqlite-sync-supabase:*`) detects the userland automatically
> and works on both. The local build-from-source flow below
> (`make postgres-supabase-build`, `Dockerfile.supabase`) currently assumes an
> Ubuntu base; if your CLI stack pulls a newer Alpine image, prefer the
> published `:17-alpine` release image.

### Prerequisites

- Supabase CLI installed (`supabase start` works)
Expand Down
Loading
Loading