Initial: Docker-Compose-Stack für Rapport Self-Hosting

Komplettes Bundle für eigene Rapport-Instanz:
- Postgres mit Supabase-Extensions + Init-Script für Standard-Rollen
- GoTrue (Auth) mit konfigurierbarem SMTP für Passwort-Reset-Mails
- PostgREST (REST-API)
- Realtime (Postgres-Changes für Live-Sync)
- Storage-API (Bilder/Quittungen)
- Kong als API-Gateway
- Rapport-Frontend als Multi-Stage-Build (zieht Sources aus dem App-Repo)

Plus:
- scripts/sync-migrations.sh: holt SQL aus dem App-Repo
- .env.example mit allen Pflicht-Secrets + optionalen SMTP-Werten
- nginx.conf mit SPA-Routing
- README mit Setup-Anleitung (Linux + macOS-Colima)
- LICENSE (AGPL-3.0)

Sync mit App-Repo: scripts/sync-migrations.sh holt die Migrations-SQL via
git clone und legt sie nach volumes/db/init/migrations/. Bei jedem
Rapport-Update erneut ausführen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 20:10:54 +02:00
commit 945e46fb03
10 changed files with 589 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
# Kopiere nach .env und ersetze die markierten Werte.
# Niemals committen — .env steht in .gitignore.
# ═══ Pflicht: Secrets ═══
# Zufallswerte generieren mit: openssl rand -hex 32
POSTGRES_PASSWORD=CHANGE-ME-mindestens-32-zufällige-zeichen
JWT_SECRET=CHANGE-ME-mindestens-32-zufällige-zeichen
ANON_KEY=CHANGE-ME-aus-jwt-secret-abgeleitet
SERVICE_ROLE_KEY=CHANGE-ME-aus-jwt-secret-abgeleitet
# ═══ Pflicht: URLs ═══
# Wenn nur LAN: SITE_URL=http://192.168.1.50:8080 (oder rapport.local:8080)
# Wenn extern via Reverse-Proxy: SITE_URL=https://app.rapport.studio.ch
SITE_URL=http://localhost:8080
API_EXTERNAL_URL=http://localhost:8000
# ═══ Optional: Ports ═══
APP_PORT=8080
KONG_HTTP_PORT=8000
KONG_HTTPS_PORT=8443
DB_PORT=5432
# ═══ Optional: Rapport-Frontend-Version ═══
# Wenn Custom-Build: setze RAPPORT_APP_TAG auf die gewünschte Version
RAPPORT_APP_TAG=0.8.2
# ═══ Optional: Email für Passwort-Reset etc. ═══
# Wenn leer: Mails landen lokal in Inbucket (Test-Mailserver, Port 9000)
# Für Production: echten SMTP-Server angeben
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASS=
SMTP_SENDER_NAME=Rapport
SMTP_ADMIN_EMAIL=admin@rapport.studio.ch
+7
View File
@@ -0,0 +1,7 @@
.env
.env.local
volumes/db/data/
volumes/db/init/migrations/
volumes/storage/
.DS_Store
*.log
+33
View File
@@ -0,0 +1,33 @@
# Multi-Stage Build für das Rapport-Frontend.
# Holt die App-Sources via git, baut sie mit Node, packt das fertige dist/
# in einen schlanken nginx-Container.
#
# Steuerung über Build-Args:
# --build-arg RAPPORT_APP_TAG=0.8.2 Version aus dem App-Repo
# --build-arg SUPABASE_URL=https://... Public-API-URL (kommt in den Build)
# --build-arg SUPABASE_ANON_KEY=... Public Anon-Key (kommt in den Build)
FROM node:20-alpine AS builder
ARG RAPPORT_APP_TAG=main
ARG SUPABASE_URL
ARG SUPABASE_ANON_KEY
ARG REPO_URL=https://git.kgva.ch/karim/RAPPORT.git
RUN apk add --no-cache git
WORKDIR /build
RUN git clone --branch "${RAPPORT_APP_TAG}" --depth 1 "${REPO_URL}" app
WORKDIR /build/app
# .env.production wird zur Build-Zeit ausgewertet → Werte landen im Bundle
RUN if [ -n "$SUPABASE_URL" ]; then \
printf "VITE_SUPABASE_URL=%s\nVITE_SUPABASE_ANON_KEY=%s\n" "$SUPABASE_URL" "$SUPABASE_ANON_KEY" > .env.production; \
fi
RUN npm install --no-audit --no-fund && npm run build
# ──────────────────────────────────────────────────────────
FROM nginx:alpine
COPY --from=builder /build/app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s CMD wget -q --spider http://localhost/ || exit 1
+24
View File
@@ -0,0 +1,24 @@
Rapport-Server — Self-Hosting-Stack für Rapport
Copyright (C) 2026 Karim Gabriele Varano
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
────────────────────────────────────────────────────────────────────────
Full license text: https://www.gnu.org/licenses/agpl-3.0.txt
The bundled Supabase containers (postgres, gotrue, postgrest, realtime,
storage-api, kong, supabase/postgres image) are distributed under their
own permissive licenses (mostly Apache-2.0 and PostgreSQL License) by the
Supabase team. See https://supabase.com/docs/guides/self-hosting for details.
+140
View File
@@ -0,0 +1,140 @@
# rapport-server
Self-Hosting-Stack für [Rapport](https://git.kgva.ch/karim/RAPPORT) — die Studio-Management-Software für Architekturbüros.
Dieses Repo enthält alles, um Rapport auf eigenem Server (Linux-VM, NAS, Mac Mini) zu hosten:
- **Postgres** (Datenbank)
- **GoTrue** (Auth — Email-Login, Passwort-Reset, …)
- **PostgREST** (REST-API auf der DB)
- **Realtime** (Live-Sync zwischen Geräten)
- **Storage** (Bilder, Quittungen)
- **Kong** (API-Gateway)
- **Rapport-Frontend** (nginx mit dem React-Build)
Alles als Docker-Compose. Komplett Open-Source.
---
## Voraussetzungen
| OS | Container-Runtime |
|---|---|
| **Linux** (Ubuntu 22.04+, Debian 12+, …) | Docker Engine + Compose v2 |
| **macOS** (Mac Mini etc.) | [Colima](https://github.com/abiosoft/colima) + Docker CLI — **vollständig Open-Source** |
> Auf macOS funktioniert auch OrbStack oder Docker Desktop, beide sind aber proprietär. Colima ist die OSS-Alternative und für Selfhost ausreichend.
Plus: ein erreichbarer DNS-Name (für TLS) — z.B. `rapport.studio.ch`. Optional: Nginx Proxy Manager oder Caddy als Reverse-Proxy für SSL.
---
## Setup
### 1. Repo klonen + Frontend-Sources holen
```bash
git clone https://git.kgva.ch/karim/rapport-server.git
cd rapport-server
```
### 2. `.env` erstellen
```bash
cp .env.example .env
```
In `.env` müssen mindestens diese drei Werte ersetzt werden:
- `POSTGRES_PASSWORD` — Datenbank-Passwort (mind. 32 Zeichen zufällig)
- `JWT_SECRET` — JWT-Signatur-Secret (mind. 32 Zeichen zufällig)
- `SITE_URL` — die öffentliche URL deiner Rapport-Instanz (z.B. `https://app.rapport.studio.ch`)
Zufallswerte generieren:
```bash
openssl rand -hex 32 # für POSTGRES_PASSWORD und JWT_SECRET
```
### 3. Migrations holen
Die SQL-Migrations stammen aus dem App-Repo. Einmal initial holen:
```bash
./scripts/sync-migrations.sh
```
(Bei späteren Rapport-Updates `sync-migrations.sh` erneut ausführen, dann `docker compose down && docker compose up -d`.)
### 4. Stack starten
```bash
docker compose up -d
```
Erststart dauert ~1 Minute (Postgres initialisiert sich, Migrations laufen, Container starten).
### 5. Health-Check
```bash
docker compose ps
```
Alle Container sollten `healthy` zeigen.
Frontend ist erreichbar auf `http://localhost:8080` — direkt im Browser öffnen oder über deinen Reverse-Proxy auf eine Domain mappen.
---
## Reverse-Proxy + HTTPS
Empfohlen: **Nginx Proxy Manager** (OSS, Web-UI, Let's-Encrypt automatisch) oder **Caddy** (config-as-code, vollautomatisch).
Beispiel Caddy:
```caddy
app.rapport.studio.ch {
reverse_proxy localhost:8080
}
api.rapport.studio.ch {
reverse_proxy localhost:8000 # Kong-Gateway
}
```
In `.env` dann `SITE_URL=https://app.rapport.studio.ch` und `API_EXTERNAL_URL=https://api.rapport.studio.ch` setzen.
---
## Updates
```bash
git pull
./scripts/sync-migrations.sh # falls neue Migrations
docker compose pull # neueste Container-Versionen
docker compose up -d # neu starten
```
Daten bleiben erhalten (Volume `postgres-data` wird nicht angetastet).
---
## Backup
Komplettes Postgres-Dump:
```bash
docker compose exec -T db pg_dumpall -U postgres > backup-$(date +%Y%m%d).sql
```
Empfohlen: per `cron` täglich + auf externe Disk/S3/Backblaze sichern.
Storage (Quittungen, Logos):
```bash
docker compose exec storage tar -czf - /var/lib/storage > storage-$(date +%Y%m%d).tar.gz
```
---
## Lizenz
GNU AGPL-3.0-or-later — identisch zur Rapport-App.
+185
View File
@@ -0,0 +1,185 @@
# Rapport-Server — Komplettstack für Self-Hosting.
#
# Bevor `docker compose up`:
# 1. cp .env.example .env und Secrets ersetzen
# 2. ./scripts/sync-migrations.sh (holt SQL aus dem App-Repo)
#
# Dann:
# docker compose up -d
#
# Erststart dauert ~1 Min (Postgres bootet, Migrations laufen, Health-Checks).
services:
# ════════════════════════════════════════════════════════════════════════
# Postgres — Datenbank mit Supabase-Extensions
# ════════════════════════════════════════════════════════════════════════
db:
image: supabase/postgres:15.8.1.020
container_name: rapport-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
- ./volumes/db/init/migrations:/docker-entrypoint-initdb.d/migrations:ro
- ./volumes/db/init/00-init.sh:/docker-entrypoint-initdb.d/00-init.sh:ro
ports:
- "${DB_PORT:-5432}:5432"
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 5s
timeout: 5s
retries: 10
# ════════════════════════════════════════════════════════════════════════
# GoTrue — Auth (Email-Login, Passwort-Reset, Magic-Links)
# ════════════════════════════════════════════════════════════════════════
auth:
image: supabase/gotrue:v2.158.1
container_name: rapport-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}/
GOTRUE_DISABLE_SIGNUP: "false"
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"
GOTRUE_SMTP_HOST: ${SMTP_HOST:-}
GOTRUE_SMTP_PORT: ${SMTP_PORT:-587}
GOTRUE_SMTP_USER: ${SMTP_USER:-}
GOTRUE_SMTP_PASS: ${SMTP_PASS:-}
GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL:-admin@rapport.local}
GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME:-Rapport}
# ════════════════════════════════════════════════════════════════════════
# PostgREST — REST-API auf der Datenbank
# ════════════════════════════════════════════════════════════════════════
rest:
image: postgrest/postgrest:v12.2.0
container_name: rapport-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,storage
PGRST_DB_ANON_ROLE: anon
PGRST_JWT_SECRET: ${JWT_SECRET}
PGRST_DB_USE_LEGACY_GUCS: "false"
PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
PGRST_APP_SETTINGS_JWT_EXP: 3600
# ════════════════════════════════════════════════════════════════════════
# Realtime — Postgres-Changes-Broadcast für Live-Sync
# ════════════════════════════════════════════════════════════════════════
realtime:
image: supabase/realtime:v2.30.34
container_name: rapport-realtime
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
PORT: 4000
DB_HOST: db
DB_PORT: 5432
DB_USER: supabase_admin
DB_PASSWORD: ${POSTGRES_PASSWORD}
DB_NAME: postgres
DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime'
DB_ENC_KEY: supabaserealtime
API_JWT_SECRET: ${JWT_SECRET}
SECRET_KEY_BASE: ${JWT_SECRET}
ERL_AFLAGS: -proto_dist inet_tcp
ENABLE_TAILSCALE: "false"
DNS_NODES: "''"
command: >
sh -c "/app/bin/migrate && /app/bin/realtime eval 'Realtime.Release.seeds(Realtime.Repo)' && /app/bin/server"
# ════════════════════════════════════════════════════════════════════════
# Storage — Object-Storage für Bilder (Quittungen, Logos)
# ════════════════════════════════════════════════════════════════════════
storage:
image: supabase/storage-api:v1.11.13
container_name: rapport-storage
restart: unless-stopped
depends_on:
db:
condition: service_healthy
rest:
condition: service_started
environment:
ANON_KEY: ${ANON_KEY}
SERVICE_KEY: ${SERVICE_ROLE_KEY}
POSTGREST_URL: http://rest:3000
PGRST_JWT_SECRET: ${JWT_SECRET}
DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@db:5432/postgres
FILE_SIZE_LIMIT: 52428800
STORAGE_BACKEND: file
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
TENANT_ID: stub
REGION: stub
GLOBAL_S3_BUCKET: stub
volumes:
- storage-data:/var/lib/storage
# ════════════════════════════════════════════════════════════════════════
# Kong — API-Gateway: bündelt alle Services unter einer URL
# ════════════════════════════════════════════════════════════════════════
kong:
image: kong:2.8.1
container_name: rapport-kong
restart: unless-stopped
depends_on:
- auth
- rest
- storage
- realtime
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:
- "${KONG_HTTP_PORT:-8000}:8000"
- "${KONG_HTTPS_PORT:-8443}:8443"
# ════════════════════════════════════════════════════════════════════════
# Rapport Frontend — die React-App hinter nginx
# ════════════════════════════════════════════════════════════════════════
app:
build:
context: .
dockerfile: Dockerfile.app
args:
RAPPORT_APP_TAG: ${RAPPORT_APP_TAG:-main}
SUPABASE_URL: ${API_EXTERNAL_URL}
SUPABASE_ANON_KEY: ${ANON_KEY}
image: rapport-app:${RAPPORT_APP_TAG:-latest}
container_name: rapport-app
restart: unless-stopped
ports:
- "${APP_PORT:-8080}:80"
volumes:
postgres-data:
storage-data:
+48
View File
@@ -0,0 +1,48 @@
# Kong-Konfiguration für den Rapport-Server-Stack.
# Routet alle Supabase-API-Pfade (/auth/v1, /rest/v1, /storage/v1, /realtime/v1)
# durch dieselbe URL, damit das Frontend nur eine Adresse kennt.
_format_version: "2.1"
_transform: true
services:
- name: auth-v1
url: http://auth:9999/
routes:
- name: auth-v1-route
strip_path: true
paths:
- /auth/v1/
plugins:
- name: cors
- name: rest-v1
url: http://rest:3000/
routes:
- name: rest-v1-route
strip_path: true
paths:
- /rest/v1/
plugins:
- name: cors
- name: storage-v1
url: http://storage:5000/
routes:
- name: storage-v1-route
strip_path: true
paths:
- /storage/v1/
plugins:
- name: cors
- name: realtime-v1
url: http://realtime:4000/socket/
protocol: http
routes:
- name: realtime-v1-route
strip_path: true
paths:
- /realtime/v1/
plugins:
- name: cors
+23
View File
@@ -0,0 +1,23 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Hash-versionierte Assets — langer Cache OK
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# SPA-Fallback: unbekannte Pfade → index.html (React-Routing-kompatibel)
location / {
add_header Cache-Control "no-cache, no-store, must-revalidate";
try_files $uri /index.html;
}
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Holt die aktuellen Postgres-Migrations aus dem RAPPORT-App-Repo.
#
# Aufruf einmal bei Setup, danach nach jedem Rapport-Update.
set -euo pipefail
cd "$(dirname "$0")/.."
REPO_URL="${RAPPORT_REPO_URL:-https://git.kgva.ch/karim/RAPPORT}"
TAG="${RAPPORT_APP_TAG:-main}"
TMPDIR=$(mktemp -d)
trap "rm -rf $TMPDIR" EXIT
echo "→ Hole Migrations aus $REPO_URL @ $TAG"
git clone --branch "$TAG" --depth 1 --quiet "$REPO_URL" "$TMPDIR/app"
if [ ! -d "$TMPDIR/app/supabase/migrations" ]; then
echo "✗ Migrations-Verzeichnis nicht gefunden im App-Repo." >&2
exit 1
fi
rm -rf volumes/db/init/migrations
mkdir -p volumes/db/init/migrations
cp "$TMPDIR/app/supabase/migrations/"*.sql volumes/db/init/migrations/
COUNT=$(ls volumes/db/init/migrations/*.sql | wc -l | tr -d ' ')
echo "$COUNT Migrations nach volumes/db/init/migrations/ kopiert"
echo
echo "Nächster Schritt: docker compose up -d (oder neustart wenn schon läuft)"
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# Postgres-Init-Script — läuft beim ersten Start des db-Containers.
#
# 1. Legt die Supabase-Standard-Rollen an (anon, authenticated, service_role,
# supabase_auth_admin, supabase_storage_admin, authenticator).
# Diese referenzieren die in den Rapport-Migrations definierten Policies.
# 2. Wendet alle Rapport-Migrations aus ./migrations/ in alphabetischer
# Reihenfolge an.
#
# Nach diesem Script ist die DB einsatzbereit.
set -euo pipefail
echo "→ Supabase-Standard-Rollen anlegen…"
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname postgres <<-EOSQL
-- Standard-Rollen (idempotent)
do \$\$ begin
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
execute format('create role authenticator noinherit login password %L', current_setting('rapport.postgres_password', true));
end if;
if not exists (select 1 from pg_roles where rolname = 'supabase_auth_admin') then
execute format('create role supabase_auth_admin login password %L', current_setting('rapport.postgres_password', true));
end if;
if not exists (select 1 from pg_roles where rolname = 'supabase_storage_admin') then
execute format('create role supabase_storage_admin login password %L', current_setting('rapport.postgres_password', true));
end if;
if not exists (select 1 from pg_roles where rolname = 'supabase_admin') then
execute format('create role supabase_admin superuser login password %L', current_setting('rapport.postgres_password', true));
end if;
end \$\$;
grant anon to authenticator;
grant authenticated to authenticator;
grant service_role to authenticator;
-- auth-Schema (für GoTrue)
create schema if not exists auth authorization supabase_auth_admin;
-- storage-Schema (für Storage-Service)
create schema if not exists storage authorization supabase_storage_admin;
-- pgcrypto + andere Extensions
create extension if not exists pgcrypto;
create extension if not exists "uuid-ossp";
EOSQL
echo "→ Rapport-Migrations applizieren…"
for f in /docker-entrypoint-initdb.d/migrations/*.sql; do
if [ -f "$f" ]; then
echo "$(basename "$f")"
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname postgres -f "$f"
fi
done
echo "✓ DB-Initialisierung abgeschlossen."