Selfhosting Readme

This commit is contained in:
2026-03-18 18:26:39 +01:00
parent 1511340f00
commit 69ff646689

518
README.md
View File

@@ -1,109 +1,439 @@
# FEDEO Hosting Guide
Diese Anleitung beschreibt ein produktionsnahes Self-Hosting von FEDEO mit Docker Compose, Traefik, PostgreSQL und optionalem S3-kompatiblem Objektspeicher via MinIO.
## Architektur
# Docker Compose Setup Der Stack besteht aus:
## ENV Vars - `frontend`: Nuxt-Frontend auf Port `3000`
- `backend`: Node/Fastify-API auf Port `3100`
- `db`: PostgreSQL
- `traefik`: Reverse Proxy mit automatischen Let's-Encrypt-Zertifikaten
- optional `minio`: S3-kompatibler Objektspeicher fur Dateiuploads
- DOMAIN Die Konfiguration erfolgt uber Umgebungsvariablen beziehungsweise eine `.env`-Datei im Deploy-Verzeichnis.
- PDF_LICENSE
- DB_PASS
- DB_USER
- CONTACT_EMAIL
## Docker Compose File ## Voraussetzungen
~~~
Vor dem Deployment sollten folgende Punkte erfullt sein:
- Ein Linux-Server oder VPS mit offentlichen Ports `80` und `443`
- Docker Engine inkl. Compose Plugin
- Eine Domain, die auf den Server zeigt, z. B. `app.example.com`
- Optional: SMTP-Zugang fur E-Mails
- Optional: S3-Bucket oder MinIO fur Dateispeicher
Empfohlen:
- mindestens 2 vCPU
- mindestens 4 GB RAM
- SSD-Speicher fur PostgreSQL und Dateiuploads
## DNS und Netzwerk
Lege mindestens einen A- oder AAAA-Record an:
- `app.example.com -> <SERVER-IP>`
Traefik terminiert TLS direkt im Compose-Stack. Es ist kein zusatzlicher Reverse Proxy davor erforderlich.
## Benotigte Backend-Umgebungsvariablen
Das Backend erwartet mindestens diese Umgebungsvariablen:
- `COOKIE_SECRET`
- `JWT_SECRET`
- `PORT`
- `HOST`
- `DATABASE_URL`
- `S3_BUCKET`
- `ENCRYPTION_KEY`
- `MAILER_SMTP_HOST`
- `MAILER_SMTP_PORT`
- `MAILER_SMTP_SSL`
- `MAILER_SMTP_USER`
- `MAILER_SMTP_PASS`
- `MAILER_FROM`
- `S3_ENDPOINT`
- `S3_REGION`
- `S3_ACCESS_KEY`
- `S3_SECRET_KEY`
- `M2M_API_KEY`
- `API_BASE_URL`
- `GOCARDLESS_BASE_URL`
- `GOCARDLESS_SECRET_ID`
- `GOCARDLESS_SECRET_KEY`
- `DOKUBOX_IMAP_HOST`
- `DOKUBOX_IMAP_PORT`
- `DOKUBOX_IMAP_SECURE`
- `DOKUBOX_IMAP_USER`
- `DOKUBOX_IMAP_PASSWORD`
- `OPENAI_API_KEY`
- `STIRLING_API_KEY`
Minimal wichtige Werte fur den ersten Start:
- `HOST=0.0.0.0`
- `PORT=3100`
- `DATABASE_URL=postgres://fedeo:<starkes-passwort>@db:5432/fedeo`
- `API_BASE_URL=https://app.example.com/backend`
Wenn du MinIO verwendest, setze zusatzlich:
- `S3_ENDPOINT=http://minio:9000`
- `S3_REGION=eu-central-1`
- `S3_ACCESS_KEY=<MINIO_ROOT_USER>`
- `S3_SECRET_KEY=<MINIO_ROOT_PASSWORD>`
- `S3_BUCKET=fedeo`
## Deploy-Struktur
Deploye den Stack direkt aus einem geklonten Checkout dieses Repositories, weil die Compose-Datei die lokalen Build-Kontexte `./frontend` und `./backend` verwendet.
Beispiel:
```bash
git clone <DEIN-REPO-URL> /opt/fedeo
cd /opt/fedeo
```
Die Verzeichnisstruktur sollte dann mindestens so aussehen:
```text
/opt/fedeo/
docker-compose.yml
.env
backend/
frontend/
traefik/
letsencrypt/
logs/
postgres/
minio/
```
Danach:
```bash
mkdir -p /opt/fedeo/traefik/letsencrypt
mkdir -p /opt/fedeo/traefik/logs
mkdir -p /opt/fedeo/postgres
mkdir -p /opt/fedeo/minio
touch /opt/fedeo/traefik/letsencrypt/acme.json
chmod 600 /opt/fedeo/traefik/letsencrypt/acme.json
```
## Beispiel `.env`
Diese Datei liegt neben der `docker-compose.yml`:
```env
DOMAIN=app.example.com
CONTACT_EMAIL=admin@example.com
DB_NAME=fedeo
DB_USER=fedeo
DB_PASSWORD=change-this-db-password
DATABASE_URL=postgres://fedeo:change-this-db-password@db:5432/fedeo
MINIO_ROOT_USER=fedeo-minio
MINIO_ROOT_PASSWORD=change-this-minio-password
MINIO_BUCKET=fedeo
HOST=0.0.0.0
PORT=3100
COOKIE_SECRET=change-this-cookie-secret
JWT_SECRET=change-this-jwt-secret
ENCRYPTION_KEY=change-this-encryption-key
MAILER_SMTP_HOST=smtp.example.com
MAILER_SMTP_PORT=587
MAILER_SMTP_SSL=false
MAILER_SMTP_USER=mailer@example.com
MAILER_SMTP_PASS=change-this-mail-password
MAILER_FROM=FEDEO <no-reply@example.com>
S3_ENDPOINT=http://minio:9000
S3_REGION=eu-central-1
S3_ACCESS_KEY=fedeo-minio
S3_SECRET_KEY=change-this-minio-password
S3_BUCKET=fedeo
M2M_API_KEY=change-this-m2m-key
API_BASE_URL=https://app.example.com/backend
GOCARDLESS_BASE_URL=https://api.gocardless.com
GOCARDLESS_SECRET_ID=replace-this
GOCARDLESS_SECRET_KEY=replace-this
DOKUBOX_IMAP_HOST=imap.example.com
DOKUBOX_IMAP_PORT=993
DOKUBOX_IMAP_SECURE=true
DOKUBOX_IMAP_USER=dokubox@example.com
DOKUBOX_IMAP_PASSWORD=change-this-imap-password
OPENAI_API_KEY=replace-this
STIRLING_API_KEY=replace-this
NUXT_PUBLIC_PDF_LICENSE=replace-with-your-pdf-license
```
## Vollstandiges Docker Compose mit optionaler S3-MinIO-Option
Hinweis: Der Stack unten startet MinIO standardmassig mit. Wenn du stattdessen AWS S3, Hetzner Object Storage, Backblaze B2 S3 oder einen anderen externen S3-Dienst nutzen willst, kannst du die Services `minio` und `createbuckets` entfernen und nur die entsprechenden S3-Umgebungsvariablen auf den externen Anbieter zeigen lassen.
```yaml
services: services:
frontend:
image: git.federspiel.tech/flfeders/fedeo/frontend:main
restart: always
environment:
- NUXT_PUBLIC_API_BASE=https://${DOMAIN}/backend
- NUXT_PUBLIC_PDF_LICENSE=${PDF_LICENSE}
networks:
- traefik
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik"
- "traefik.port=3000"
# Middlewares
- "traefik.http.middlewares.fedeo-frontend-redirect-web-secure.redirectscheme.scheme=https"
# Web Entrypoint
- "traefik.http.routers.fedeo-frontend.middlewares=fedeo-frontend-redirect-web-secure"
- "traefik.http.routers.fedeo-frontend.rule=Host(`${DOMAIN}`) && PathPrefix(`/`)"
- "traefik.http.routers.fedeo-frontend.entrypoints=web"
# Web Secure Entrypoint
- "traefik.http.routers.fedeo-frontend-secure.rule=Host(`${DOMAIN}`) && PathPrefix(`/`)"
- "traefik.http.routers.fedeo-frontend-secure.entrypoints=web-secured" #
- "traefik.http.routers.fedeo-frontend-secure.tls.certresolver=mytlschallenge"
backend:
image: git.federspiel.tech/flfeders/fedeo/backend:main
restart: always
environment:
- INFISICAL_CLIENT_ID=
- INFISICAL_CLIENT_SECRET=
- NODE_ENV=production
networks:
- traefik
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik"
- "traefik.port=3100"
# Middlewares
- "traefik.http.middlewares.fedeo-backend-redirect-web-secure.redirectscheme.scheme=https"
- "traefik.http.middlewares.fedeo-backend-strip.stripprefix.prefixes=/backend"
# Web Entrypoint
- "traefik.http.routers.fedeo-backend.middlewares=fedeo-backend-redirect-web-secure"
- "traefik.http.routers.fedeo-backend.rule=Host(`${DOMAIN}`) && PathPrefix(`/backend`)"
- "traefik.http.routers.fedeo-backend.entrypoints=web"
# Web Secure Entrypoint
- "traefik.http.routers.fedeo-backend-secure.rule=Host(`${DOMAIN}`) && PathPrefix(`/backend`)"
- "traefik.http.routers.fedeo-backend-secure.entrypoints=web-secured" #
- "traefik.http.routers.fedeo-backend-secure.tls.certresolver=mytlschallenge"
- "traefik.http.routers.fedeo-backend-secure.middlewares=fedeo-backend-strip"
# db:
# image: postgres
# restart: always
# shm_size: 128mb
# environment:
# POSTGRES_PASSWORD:
# POSTGRES_USER:
# POSTGRES_DB:
# volumes:
# - ./pg-data:/var/lib/postgresql/data
# ports:
# - "5432:5432"
traefik: traefik:
image: traefik:v2.11 image: traefik:v2.11
container_name: fedeo-traefik
restart: unless-stopped restart: unless-stopped
container_name: traefik
command: command:
- "--api.insecure=false" - --api.insecure=false
- "--api.dashboard=false" - --api.dashboard=false
- "--api.debug=false" - --providers.docker=true
- "--providers.docker=true" - --providers.docker.exposedbydefault=false
- "--providers.docker.exposedbydefault=false" - --entrypoints.web.address=:80
- "--providers.docker.network=traefik" - --entrypoints.websecure.address=:443
- "--entrypoints.web.address=:80" - --entrypoints.web.http.redirections.entrypoint.to=websecure
- "--entrypoints.web-secured.address=:443" - --entrypoints.web.http.redirections.entrypoint.scheme=https
- "--accesslog=true" - --certificatesresolvers.letsencrypt.acme.tlschallenge=true
- "--accesslog.filepath=/logs/access.log" - --certificatesresolvers.letsencrypt.acme.email=${CONTACT_EMAIL}
- "--accesslog.bufferingsize=5000" - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
- "--accesslog.fields.defaultMode=keep" - --accesslog=true
- "--accesslog.fields.headers.defaultMode=keep" - --accesslog.filepath=/logs/access.log
- "--certificatesresolvers.mytlschallenge.acme.tlschallenge=true" #
- "--certificatesresolvers.mytlschallenge.acme.email=${CONTACT_EMAIL}"
- "--certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json"
ports: ports:
- 80:80 - "80:80"
- 443:443 - "443:443"
volumes: volumes:
- "./traefik/letsencrypt:/letsencrypt" # <== Volume for certs (TLS) - ./traefik/letsencrypt:/letsencrypt
- "/var/run/docker.sock:/var/run/docker.sock:ro" - ./traefik/logs:/logs
- "./traefik/logs:/logs" - /var/run/docker.sock:/var/run/docker.sock:ro
networks: networks:
- traefik - web
db:
image: postgres:16
container_name: fedeo-db
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- ./postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
interval: 10s
timeout: 5s
retries: 10
networks:
- internal
minio:
image: minio/minio:latest
container_name: fedeo-minio
restart: unless-stopped
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
volumes:
- ./minio:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 10s
timeout: 5s
retries: 10
networks:
- internal
createbuckets:
image: minio/mc:latest
container_name: fedeo-minio-init
depends_on:
minio:
condition: service_healthy
entrypoint: >
/bin/sh -c "
mc alias set local http://minio:9000 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD};
mc mb --ignore-existing local/${MINIO_BUCKET};
mc anonymous set private local/${MINIO_BUCKET};
exit 0;
"
restart: "no"
networks:
- internal
backend:
build:
context: ./backend
container_name: fedeo-backend
restart: unless-stopped
depends_on:
db:
condition: service_healthy
minio:
condition: service_healthy
createbuckets:
condition: service_completed_successfully
environment:
NODE_ENV: production
HOST: ${HOST}
PORT: ${PORT}
COOKIE_SECRET: ${COOKIE_SECRET}
JWT_SECRET: ${JWT_SECRET}
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
DATABASE_URL: ${DATABASE_URL}
MAILER_SMTP_HOST: ${MAILER_SMTP_HOST}
MAILER_SMTP_PORT: ${MAILER_SMTP_PORT}
MAILER_SMTP_SSL: ${MAILER_SMTP_SSL}
MAILER_SMTP_USER: ${MAILER_SMTP_USER}
MAILER_SMTP_PASS: ${MAILER_SMTP_PASS}
MAILER_FROM: ${MAILER_FROM}
S3_ENDPOINT: ${S3_ENDPOINT}
S3_REGION: ${S3_REGION}
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
S3_SECRET_KEY: ${S3_SECRET_KEY}
S3_BUCKET: ${S3_BUCKET}
M2M_API_KEY: ${M2M_API_KEY}
API_BASE_URL: ${API_BASE_URL}
GOCARDLESS_BASE_URL: ${GOCARDLESS_BASE_URL}
GOCARDLESS_SECRET_ID: ${GOCARDLESS_SECRET_ID}
GOCARDLESS_SECRET_KEY: ${GOCARDLESS_SECRET_KEY}
DOKUBOX_IMAP_HOST: ${DOKUBOX_IMAP_HOST}
DOKUBOX_IMAP_PORT: ${DOKUBOX_IMAP_PORT}
DOKUBOX_IMAP_SECURE: ${DOKUBOX_IMAP_SECURE}
DOKUBOX_IMAP_USER: ${DOKUBOX_IMAP_USER}
DOKUBOX_IMAP_PASSWORD: ${DOKUBOX_IMAP_PASSWORD}
OPENAI_API_KEY: ${OPENAI_API_KEY}
STIRLING_API_KEY: ${STIRLING_API_KEY}
labels:
- traefik.enable=true
- traefik.http.routers.fedeo-backend.rule=Host(`${DOMAIN}`) && PathPrefix(`/backend`)
- traefik.http.routers.fedeo-backend.entrypoints=websecure
- traefik.http.routers.fedeo-backend.tls.certresolver=letsencrypt
- traefik.http.middlewares.fedeo-backend-strip.stripprefix.prefixes=/backend
- traefik.http.routers.fedeo-backend.middlewares=fedeo-backend-strip
- traefik.http.services.fedeo-backend.loadbalancer.server.port=3100
networks:
- web
- internal
frontend:
build:
context: ./frontend
container_name: fedeo-frontend
restart: unless-stopped
depends_on:
- backend
environment:
NODE_ENV: production
NUXT_PUBLIC_API_BASE: https://${DOMAIN}/backend
NUXT_PUBLIC_PDF_LICENSE: ${NUXT_PUBLIC_PDF_LICENSE}
labels:
- traefik.enable=true
- traefik.http.routers.fedeo-frontend.rule=Host(`${DOMAIN}`)
- traefik.http.routers.fedeo-frontend.entrypoints=websecure
- traefik.http.routers.fedeo-frontend.tls.certresolver=letsencrypt
- traefik.http.services.fedeo-frontend.loadbalancer.server.port=3000
networks:
- web
networks: networks:
traefik: web:
external: false driver: bridge
~~~ internal:
driver: bridge
```
## Externe S3-Provider statt MinIO
Wenn du keinen lokalen MinIO-Container betreiben willst:
1. Entferne die Services `minio` und `createbuckets` aus der Compose-Datei.
2. Entferne im Backend `depends_on` fur `minio` und `createbuckets`.
3. Trage in `.env` die Zugangsdaten des externen S3-Dienstes ein.
Beispiel fur die relevanten Werte:
```env
S3_ENDPOINT=https://s3.eu-central-1.amazonaws.com
S3_REGION=eu-central-1
S3_ACCESS_KEY=...
S3_SECRET_KEY=...
S3_BUCKET=fedeo
```
Hinweis: Das Backend nutzt `forcePathStyle: true`. Das funktioniert sauber mit MinIO und vielen S3-kompatiblen Providern. Bei reinem AWS S3 kann je nach Endpoint-Setup ein abweichendes Verhalten sinnvoll sein. Falls du AWS S3 einsetzen willst, sollte die S3-Initialisierung im Backend gegen den konkreten Zielprovider getestet werden.
## Start des Stacks
Im Deploy-Verzeichnis:
```bash
docker compose build
docker compose up -d
```
Danach Status prufen:
```bash
docker compose ps
docker compose logs -f traefik
docker compose logs -f backend
```
## Funktionsprufung
Nach dem ersten Start sollten mindestens diese Checks erfolgreich sein:
```bash
curl -I https://app.example.com
curl https://app.example.com/backend/health
```
Erwartung:
- Frontend liefert `200` oder `302`
- Backend liefert JSON wie `{"status":"ok"}`
## Updates
Bei neuen Versionen:
```bash
git pull
docker compose build
docker compose up -d
```
Falls du statt lokaler Builds vorgebaute Images verwenden willst, kannst du in der Compose-Datei `build:` durch passende `image:`-Eintrage ersetzen. Erst dann ist ein vorgelagertes `docker compose pull` sinnvoll.
## Backup-Empfehlung
Regelmassig sichern:
- `./postgres`
- `./minio` falls MinIO lokal genutzt wird
- `./traefik/letsencrypt/acme.json`
- deine `.env`
- deine dokumentierten Secret-Werte aus der `.env` oder deinem Secret-Management
## Bekannte Betriebsbesonderheiten
- Das Backend startet nur sauber, wenn alle Pflichtvariablen gesetzt sind.
- Ohne korrekt gesetzte S3-Secrets funktionieren Dateiuploads und dateibasierte Funktionen nicht.
- Fur die Frontend-PDF-Funktion wird eine gueltige `NUXT_PUBLIC_PDF_LICENSE` benotigt.
- PostgreSQL ist im Projekt vorgesehen; andere SQL-Datenbanken sind in dieser Compose-Datei nicht berucksichtigt.
## Optional: Nur mit bestehender externer Infrastruktur
Wenn bereits vorhanden:
- externer Reverse Proxy
- externer PostgreSQL-Server
- externer S3-Speicher
- externe Zertifikatsverwaltung
dann konnen `traefik`, `db` und `minio` aus dem Stack entfernt werden. In diesem Fall mussen die zugehorigen Hostnamen und Zugangsdaten in der `.env` beziehungsweise im Frontend-Environment auf die externe Infrastruktur zeigen.