Files
OPENBUREAU/cms/docker-compose.yml
T
karim 9163f5c90d security: public Deploy härten (Reverse-Proxy, GoTrue-Rate-Limit, RLS-Revoke)
Für die öffentlich erreichbare Instanz (dev.openbureau.ch):

1. Reverse-Proxy nur /auth/* durchreichen — /rest, /storage, /realtime raus.
   PostgREST /rest/v1/ gab die komplette DB-Schema-Beschreibung (OpenAPI) preis;
   der Browser nutzt Supabase nur fürs Login, Daten laufen über /api/*.
   (Caddy-Block in create-openbureau-lxc.sh + proxmox/README.md angepasst.)
2. GoTrue GOTRUE_RATE_LIMIT_TOKEN_REFRESH=100 — bremst Brute-Force aufs /token,
   das public direkt gegen GoTrue läuft (nicht übers Node-Rate-Limit).
3. db/schema.sql: revoke all from anon/authenticated auf posts/comments/forums/
   threads; grants nur noch service_role. RLS bleibt so auch bei künftigen
   Policies dicht (Defense-in-Depth statt "RLS ohne Policy").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 05:11:19 +02:00

187 lines
9.0 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# OPENBUREAU — Komplettstack für Self-Hosting (Muster gespiegelt von RAPPORT-SERVER).
# ALLES in einem Stack / einem LXC: Supabase-Kern + CMS.
#
# Vor `docker compose up`:
# 1. cp .env.example .env
# 2. JWT_SECRET + POSTGRES_PASSWORD setzen (openssl rand -hex 32)
# 3. node scripts/generate-keys.mjs → ANON_KEY + SERVICE_ROLE_KEY in .env
# 4. SITE_URL + API_EXTERNAL_URL auf die LAN-/Domain-Adresse setzen
# 5. kong.yml: __CORS_ORIGIN__ durch SITE_URL ersetzen (Browser-Origin)
# 6. BIND_ADDR: 127.0.0.1 hinter Reverse-Proxy, 0.0.0.0 für LAN-Direkt
#
# (Das Proxmox-Script erledigt 16 automatisch.)
# Dann: docker compose up -d --build
#
# Abweichung von RAPPORT: realtime + storage weggelassen (nutzt das CMS nicht).
# Nachrüsten = die beiden Service-Blöcke aus RAPPORT-SERVER hier einfügen.
services:
# ════════════════════════════════════════════════════════════════════════
# Postgres — Datenbank mit Supabase-Extensions
# ════════════════════════════════════════════════════════════════════════
db:
image: supabase/postgres:15.8.1.020
container_name: openbureau-db
restart: unless-stopped
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: postgres
JWT_SECRET: ${JWT_SECRET}
JWT_EXP: 3600
volumes:
- postgres-data:/var/lib/postgresql/data
# OPENBUREAU-Schema (posts-Tabelle) wird vom Post-Init eingespielt.
- ./db/schema.sql:/openbureau-schema.sql:ro
# Läuft als LETZTES Init-Script (zz-) — nach den supabase-internen.
- ./volumes/db/init/zz-openbureau-post-init.sh:/docker-entrypoint-initdb.d/zz-openbureau-post-init.sh:ro
ports:
# Nur localhost — Zugriff über Kong / interne Container-Kommunikation.
- "127.0.0.1:${DB_PORT:-5432}:5432"
healthcheck:
test: ["CMD", "pg_isready", "-U", "supabase_admin", "-d", "postgres"]
interval: 5s
timeout: 5s
retries: 20
start_period: 30s
# ════════════════════════════════════════════════════════════════════════
# Migrate — spielt das (idempotente) Schema bei jedem `up` nach, damit neue
# Tabellen/Spalten auch in eine BESTEHENDE DB kommen (Init-Scripts laufen nur
# beim allerersten Start). Läuft einmal und beendet sich. supabase_admin =
# Superuser → keine Owner-Konflikte. schema.sql ist idempotent.
# ════════════════════════════════════════════════════════════════════════
migrate:
image: supabase/postgres:15.8.1.020
container_name: openbureau-migrate
restart: "no"
depends_on:
db:
condition: service_healthy
environment:
PGPASSWORD: ${POSTGRES_PASSWORD}
volumes:
- ./db/schema.sql:/openbureau-schema.sql:ro
entrypoint: ["bash", "-c"]
command:
- >
psql -h db -U supabase_admin -d postgres -v ON_ERROR_STOP=1 -f /openbureau-schema.sql &&
psql -h db -U supabase_admin -d postgres -c "notify pgrst, 'reload schema';" &&
echo '✓ Schema migriert.'
# ════════════════════════════════════════════════════════════════════════
# GoTrue — Auth (Login für das CMS)
# ════════════════════════════════════════════════════════════════════════
auth:
image: supabase/gotrue:v2.158.1
container_name: openbureau-auth
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
GOTRUE_API_HOST: 0.0.0.0
GOTRUE_API_PORT: 9999
API_EXTERNAL_URL: ${API_EXTERNAL_URL}
GOTRUE_DB_DRIVER: postgres
GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@db:5432/postgres
GOTRUE_SITE_URL: ${SITE_URL}
GOTRUE_URI_ALLOW_LIST: ${SITE_URL},${SITE_URL}/
# Single-Author: Self-Signup aus. User wird per Admin-API angelegt
# (Kommando steht im README / LXC-Output).
GOTRUE_DISABLE_SIGNUP: "true"
# Brute-Force-Bremse aufs /token: das öffentliche Login läuft direkt gegen
# GoTrue (nicht über das Node-Rate-Limit), daher hier kappen — max. 100
# Token-Anfragen / 5 Min. Reichlich für einen Autor, bremst Rateversuche.
GOTRUE_RATE_LIMIT_TOKEN_REFRESH: "100"
GOTRUE_JWT_ADMIN_ROLES: service_role
GOTRUE_JWT_AUD: authenticated
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
GOTRUE_JWT_EXP: 3600
GOTRUE_JWT_SECRET: ${JWT_SECRET}
GOTRUE_EXTERNAL_EMAIL_ENABLED: "true"
GOTRUE_MAILER_AUTOCONFIRM: "true"
# ════════════════════════════════════════════════════════════════════════
# PostgREST — REST-API auf der DB (supabase-js spricht hierüber mit posts)
# ════════════════════════════════════════════════════════════════════════
rest:
image: postgrest/postgrest:v12.2.0
container_name: openbureau-rest
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@db:5432/postgres
PGRST_DB_SCHEMAS: public
PGRST_DB_ANON_ROLE: anon
PGRST_JWT_SECRET: ${JWT_SECRET}
PGRST_DB_USE_LEGACY_GUCS: "false"
# ════════════════════════════════════════════════════════════════════════
# Kong — API-Gateway: bündelt /auth/v1 + /rest/v1 unter einer URL
# ════════════════════════════════════════════════════════════════════════
kong:
image: kong:2.8.1
container_name: openbureau-kong
restart: unless-stopped
depends_on:
- auth
- rest
environment:
KONG_DATABASE: "off"
KONG_DECLARATIVE_CONFIG: /var/lib/kong/kong.yml
KONG_DNS_ORDER: LAST,A,CNAME
KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth
volumes:
- ./kong.yml:/var/lib/kong/kong.yml:ro
ports:
# Standard 127.0.0.1: nur lokal/Reverse-Proxy erreichbar. Für LAN-Direkt-
# zugriff ohne Proxy BIND_ADDR=0.0.0.0 in .env setzen.
- "${BIND_ADDR:-127.0.0.1}:${KONG_HTTP_PORT:-8000}:8000"
- "${BIND_ADDR:-127.0.0.1}:${KONG_HTTPS_PORT:-8443}:8443"
# ════════════════════════════════════════════════════════════════════════
# CMS — Node-API + Hugo-Binary + Admin-SPA, serviert die Site
# ════════════════════════════════════════════════════════════════════════
cms:
build:
context: .
dockerfile: api/Dockerfile
args:
# Browser-seitig (Admin-SPA, zur Build-Zeit): öffentliche Supabase-URL.
VITE_SUPABASE_URL: ${API_EXTERNAL_URL}
VITE_SUPABASE_ANON_KEY: ${ANON_KEY}
container_name: openbureau-cms
restart: unless-stopped
depends_on:
db:
condition: service_healthy
migrate:
condition: service_completed_successfully
kong:
condition: service_started
environment:
# Server-seitig: intern über Kong, mit Service-Key.
SUPABASE_URL: http://kong:8000
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
# Für lokale JWT-Verifikation (kein GoTrue-Roundtrip pro Request).
JWT_SECRET: ${JWT_SECRET}
ADMIN_EMAILS: ${ADMIN_EMAILS:-}
SITE_DIR: /site
PORT: 3000
GIT_PUBLISH: ${GIT_PUBLISH:-false}
GIT_REMOTE: ${GIT_REMOTE:-origin}
GIT_BRANCH: ${GIT_BRANCH:-main}
GIT_AUTHOR_NAME: ${GIT_AUTHOR_NAME:-OPENBUREAU CMS}
GIT_AUTHOR_EMAIL: ${GIT_AUTHOR_EMAIL:-cms@openbureau.ch}
volumes:
# Repo-Root: api schreibt content/ und baut public/ + preview/.
- ..:/site
ports:
# Wie Kong: standardmäßig nur 127.0.0.1 (hinter Reverse-Proxy).
- "${BIND_ADDR:-127.0.0.1}:${APP_PORT:-8080}:3000"
volumes:
postgres-data: