feat(deploy): ambrosio production stack (erosi.oier.ovh)

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.
This commit is contained in:
2026-04-14 19:41:33 +02:00
parent 2db5aaac14
commit 74e919aa71
8 changed files with 2494 additions and 38 deletions

View File

@@ -1,32 +1,159 @@
#!/bin/bash
# Runs once on first container start (docker-entrypoint-initdb.d).
# Sets Supabase internal role passwords to POSTGRES_PASSWORD and creates
# required schemas with correct grants.
# 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}"
psql -v ON_ERROR_STOP=1 --username supabase_admin --dbname postgres <<-EOSQL
-- Set passwords for all Supabase internal roles.
-- The supabase/postgres image creates them with hardcoded defaults;
-- this overrides them so services connect with POSTGRES_PASSWORD.
ALTER ROLE authenticator WITH PASSWORD '${PASS}';
ALTER ROLE supabase_auth_admin WITH PASSWORD '${PASS}';
ALTER ROLE supabase_replication_admin WITH PASSWORD '${PASS}';
ALTER ROLE supabase_storage_admin WITH PASSWORD '${PASS}';
ALTER ROLE pgbouncer WITH PASSWORD '${PASS}';
# 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 the _realtime schema required by supabase/realtime
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. Realtime's per-tenant migrations
-- expect it to exist before running (they don't CREATE SCHEMA themselves)
-- and without it every tenant connect fails with "schema realtime does
-- not exist" → PoolingReplicationError → no postgres_changes delivery.
-- 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
echo "==> db-init: role passwords and schemas configured"
# 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"

View File

@@ -1,11 +0,0 @@
-- Set passwords for Supabase internal roles to match POSTGRES_PASSWORD.
-- The supabase/postgres image creates these roles with hardcoded default passwords;
-- this script overrides them so all services can authenticate with POSTGRES_PASSWORD.
-- Runs once on first container start (docker-entrypoint-initdb.d).
-- Must run as supabase_admin (superuser) which this image uses for initdb scripts.
ALTER ROLE authenticator WITH PASSWORD 'REPLACE_PASSWORD';
ALTER ROLE supabase_auth_admin WITH PASSWORD 'REPLACE_PASSWORD';
ALTER ROLE supabase_replication_admin WITH PASSWORD 'REPLACE_PASSWORD';
ALTER ROLE supabase_storage_admin WITH PASSWORD 'REPLACE_PASSWORD';
ALTER ROLE pgbouncer WITH PASSWORD 'REPLACE_PASSWORD';