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:
@@ -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"
|
||||
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user