Self-contained compose for a single-VPS deploy behind the host's Caddy. App + Supabase API share erosi.oier.ovh; Keycloak on auth.oier.ovh. - infra/docker-compose.erosi.yml — full stack bound to 127.0.0.1 only (db, auth, rest, realtime, storage, imgproxy, kong, meta, keycloak, app) - infra/caddy/erosi.Caddyfile — host-Caddy snippet with TLS + path routing (/auth /rest /storage /realtime /graphql → kong, rest → app) - infra/scripts/deploy-erosi.sh — rsync + first-run secret generation + build + migrations + Caddy snippet install - keycloak/realm-export.erosi.json — prod redirect URIs, registration on, client secret as __KEYCLOAK_CLIENT_SECRET__ placeholder (deploy sub'd) - .env.erosi.example — env template with placeholders Supporting fixes to make the deploy work on a clean Supabase-postgres volume: - infra/db-init/00-role-passwords.sh — rewrote as idempotent bootstrap: CREATE the Supabase service roles if missing, set passwords on pre-existing ones, enable pg_cron/pgcrypto/uuid-ossp, create auth/storage/graphql_public/ _realtime/realtime schemas, create empty supabase_realtime publication, set per-role search_path (auth→auth, storage→storage). Old version only ALTERed passwords and relied on the roles already existing — worked for a grandfathered dev volume, failed on a fresh prod init. - infra/db-init/00-role-passwords.sql — removed (folded into .sh). - apps/web/vite.config.ts — PWA strategy generateSW (was injectManifest). The plugin's injectManifest hardcodes the SW source filename to service-worker.js and ignores the filename: 'sw.ts' override, making the production build fail. generateSW's auto-generated precache-only SW is functionally equivalent to our 5-line src/sw.ts. Tested end-to-end on ambrosio: all 9 containers up, 11 migrations applied, https://erosi.oier.ovh returns 200, Keycloak OIDC discovery serves the correct issuer, /auth/v1/settings lists Keycloak as the sole external provider.
160 lines
7.6 KiB
Bash
Executable File
160 lines
7.6 KiB
Bash
Executable File
#!/bin/bash
|
|
# Runs once on first container start (docker-entrypoint-initdb.d).
|
|
# Two cases:
|
|
# 1. Fresh volume (e.g. prod on ambrosio): the supabase/postgres image's
|
|
# initdb only creates the `postgres` role. Create all Supabase internal
|
|
# roles here, then set their passwords.
|
|
# 2. Pre-existing volume carried over from an older build that already has
|
|
# the roles (e.g. long-lived dev): only ALTER their passwords.
|
|
#
|
|
# Idempotent: safe on both paths.
|
|
|
|
set -e
|
|
|
|
PASS="${POSTGRES_PASSWORD:-postgres}"
|
|
|
|
# Connect as `postgres` (always exists post-initdb) on the Unix socket (trusted).
|
|
psql -v ON_ERROR_STOP=1 --username postgres --dbname postgres <<-EOSQL
|
|
-- ── Role bootstrap (idempotent) ───────────────────────────────────────
|
|
-- Template for most service roles: NOLOGIN OR LOGIN depending on use.
|
|
|
|
DO \$\$ BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_admin') THEN
|
|
CREATE ROLE supabase_admin SUPERUSER CREATEDB CREATEROLE REPLICATION BYPASSRLS LOGIN PASSWORD '${PASS}';
|
|
ELSE
|
|
ALTER ROLE supabase_admin WITH PASSWORD '${PASS}';
|
|
END IF;
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'anon') THEN
|
|
CREATE ROLE anon NOLOGIN NOINHERIT;
|
|
END IF;
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated') THEN
|
|
CREATE ROLE authenticated NOLOGIN NOINHERIT;
|
|
END IF;
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN
|
|
CREATE ROLE service_role NOLOGIN NOINHERIT BYPASSRLS;
|
|
END IF;
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticator') THEN
|
|
CREATE ROLE authenticator LOGIN NOINHERIT PASSWORD '${PASS}';
|
|
ELSE
|
|
ALTER ROLE authenticator WITH PASSWORD '${PASS}';
|
|
END IF;
|
|
-- authenticator must be able to SET ROLE to anon/authenticated/service_role.
|
|
GRANT anon, authenticated, service_role TO authenticator;
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_auth_admin') THEN
|
|
CREATE ROLE supabase_auth_admin LOGIN NOINHERIT CREATEROLE PASSWORD '${PASS}';
|
|
ELSE
|
|
ALTER ROLE supabase_auth_admin WITH PASSWORD '${PASS}';
|
|
END IF;
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_storage_admin') THEN
|
|
CREATE ROLE supabase_storage_admin LOGIN NOINHERIT CREATEROLE PASSWORD '${PASS}';
|
|
ELSE
|
|
ALTER ROLE supabase_storage_admin WITH PASSWORD '${PASS}';
|
|
END IF;
|
|
GRANT authenticator TO supabase_storage_admin;
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_replication_admin') THEN
|
|
CREATE ROLE supabase_replication_admin LOGIN REPLICATION PASSWORD '${PASS}';
|
|
ELSE
|
|
ALTER ROLE supabase_replication_admin WITH PASSWORD '${PASS}';
|
|
END IF;
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_realtime_admin') THEN
|
|
CREATE ROLE supabase_realtime_admin NOLOGIN NOINHERIT;
|
|
END IF;
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'pgbouncer') THEN
|
|
CREATE ROLE pgbouncer LOGIN PASSWORD '${PASS}';
|
|
ELSE
|
|
ALTER ROLE pgbouncer WITH PASSWORD '${PASS}';
|
|
END IF;
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'dashboard_user') THEN
|
|
CREATE ROLE dashboard_user CREATEROLE CREATEDB REPLICATION;
|
|
END IF;
|
|
END \$\$;
|
|
|
|
-- Grant postgres membership in service roles so seed inserts work.
|
|
GRANT anon, authenticated, service_role, supabase_auth_admin, supabase_storage_admin, supabase_realtime_admin TO postgres;
|
|
|
|
-- Per-role search_path + timeouts (match dev to avoid migration drift).
|
|
-- GoTrue's migrations issue unqualified CREATE TYPE / CREATE TABLE expecting
|
|
-- these to land in the `auth` schema — with no default here they end up in
|
|
-- `public` and later migrations that reference `auth.factor_type` blow up.
|
|
ALTER ROLE supabase_auth_admin SET search_path TO auth;
|
|
ALTER ROLE supabase_auth_admin SET idle_in_transaction_session_timeout TO 60000;
|
|
ALTER ROLE supabase_storage_admin SET search_path TO storage;
|
|
ALTER ROLE anon SET statement_timeout TO '3s';
|
|
ALTER ROLE authenticated SET statement_timeout TO '8s';
|
|
ALTER ROLE authenticator SET statement_timeout TO '8s';
|
|
ALTER ROLE authenticator SET lock_timeout TO '8s';
|
|
|
|
-- ── Extensions the migrations assume are enabled ─────────────────────
|
|
-- Migration 005 invokes cron.schedule(...); dev has pg_cron auto-enabled
|
|
-- via the image's supa-init scripts, prod does not.
|
|
CREATE EXTENSION IF NOT EXISTS pg_cron;
|
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
|
|
-- ── Schemas the Supabase services expect ─────────────────────────────
|
|
CREATE SCHEMA IF NOT EXISTS auth AUTHORIZATION supabase_auth_admin;
|
|
CREATE SCHEMA IF NOT EXISTS storage AUTHORIZATION supabase_storage_admin;
|
|
CREATE SCHEMA IF NOT EXISTS graphql_public;
|
|
|
|
CREATE SCHEMA IF NOT EXISTS _realtime;
|
|
GRANT ALL ON SCHEMA _realtime TO supabase_replication_admin;
|
|
ALTER ROLE supabase_replication_admin SET search_path TO _realtime;
|
|
|
|
-- Pre-create the `realtime` schema so Realtime's per-tenant migrations
|
|
-- don't fail on "schema realtime does not exist".
|
|
CREATE SCHEMA IF NOT EXISTS realtime AUTHORIZATION supabase_admin;
|
|
|
|
-- Empty publication that migration 007 will ALTER with specific tables.
|
|
-- (supabase/postgres image ships this pre-created in newer versions;
|
|
-- on v15.1.1.78 it does not.)
|
|
CREATE PUBLICATION supabase_realtime;
|
|
|
|
-- PostgREST needs USAGE on public for anon + authenticated + service_role.
|
|
GRANT USAGE ON SCHEMA public TO anon, authenticated, service_role;
|
|
|
|
-- Default privileges for future tables in public (RLS policies still apply).
|
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO anon, authenticated, service_role;
|
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO anon, authenticated, service_role;
|
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon, authenticated, service_role;
|
|
|
|
-- GoTrue runs its own migrations against `public.schema_migrations`. PG 15
|
|
-- revokes CREATE from public by default, so grant it back to the service roles.
|
|
GRANT CREATE, USAGE ON SCHEMA public TO supabase_auth_admin, supabase_storage_admin;
|
|
|
|
-- Storage migrations call statements that require database-level grants
|
|
-- (e.g. CREATE EXTENSION, ALTER DEFAULT PRIVILEGES).
|
|
GRANT ALL ON DATABASE postgres TO supabase_auth_admin, supabase_storage_admin;
|
|
|
|
-- ── Keycloak database + role (separate DB, idempotent) ───────────────
|
|
-- CREATE DATABASE cannot run in a transaction, so run it via \gexec.
|
|
SELECT 'CREATE DATABASE keycloak'
|
|
WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'keycloak') \gexec
|
|
|
|
DO \$\$ BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'keycloak') THEN
|
|
CREATE ROLE keycloak LOGIN PASSWORD '${PASS}';
|
|
ELSE
|
|
ALTER ROLE keycloak WITH PASSWORD '${PASS}';
|
|
END IF;
|
|
END \$\$;
|
|
|
|
GRANT ALL PRIVILEGES ON DATABASE keycloak TO keycloak;
|
|
EOSQL
|
|
|
|
# Keycloak also needs CREATE on the keycloak database's public schema.
|
|
psql -v ON_ERROR_STOP=1 --username postgres --dbname keycloak <<-EOSQL
|
|
GRANT ALL ON SCHEMA public TO keycloak;
|
|
EOSQL
|
|
|
|
echo "==> db-init: roles bootstrapped, passwords set, schemas created"
|