# 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 Der Stack besteht aus: - `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 Die Konfiguration erfolgt uber Umgebungsvariablen beziehungsweise eine `.env`-Datei im Deploy-Verzeichnis. ## 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 -> ` 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:@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=` - `S3_SECRET_KEY=` - `S3_BUCKET=fedeo` ## Deploy-Struktur Deploye den Stack direkt aus einem geklonten Checkout dieses Repositories, weil die Selfhost-Compose-Datei die lokalen Build-Kontexte `./frontend` und `./backend` verwendet. Beispiel: ```bash git clone /opt/fedeo cd /opt/fedeo ``` Die Verzeichnisstruktur sollte dann mindestens so aussehen: ```text /opt/fedeo/ docker-compose.selfhost.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 ``` Als Startpunkt kannst du die Beispielumgebung kopieren: ```bash cp .env.example .env ``` Ersetze anschließend alle Platzhalter und passe mindestens `DOMAIN`, `CONTACT_EMAIL`, Datenbank-, Secret-, SMTP- und S3-Werte an. ## Beispiel `.env` Diese Datei liegt neben der `docker-compose.yml`: ```env DOMAIN=app.example.com CONTACT_EMAIL=admin@deine-domain.de 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 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 FEDEO_BOOTSTRAP_ADMIN_EMAIL=admin@example.com FEDEO_BOOTSTRAP_ADMIN_PASSWORD=change-this-admin-password FEDEO_BOOTSTRAP_ADMIN_FIRST_NAME=Admin FEDEO_BOOTSTRAP_ADMIN_LAST_NAME=Benutzer FEDEO_BOOTSTRAP_TENANT_NAME=Mein Unternehmen FEDEO_BOOTSTRAP_TENANT_SHORT=MEIN ``` Die `FEDEO_BOOTSTRAP_*`-Werte sind für den ersten Start gedacht. Wenn `FEDEO_BOOTSTRAP_ADMIN_EMAIL` und `FEDEO_BOOTSTRAP_ADMIN_PASSWORD` gesetzt sind, legt das Backend idempotent einen Admin-Benutzer, einen ersten Mandanten, eine Administrator-Rolle und grundlegende Stammdaten an. Nach erfolgreichem Erstzugriff solltest du das Bootstrap-Passwort aus der `.env` entfernen oder ändern. ## Docker Compose mit optionaler S3-MinIO-Option Die Selfhost-Konfiguration liegt in `docker-compose.selfhost.yml`. Sie startet MinIO standardmäßig 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. Das Backend führt beim Containerstart standardmäßig `npm run migrate` aus. Setze `FEDEO_RUN_MIGRATIONS=false`, wenn du Migrationen bewusst manuell ausführen möchtest. ```yaml services: traefik: image: traefik:v2.11 container_name: fedeo-traefik restart: unless-stopped command: - --api.insecure=false - --api.dashboard=false - --providers.docker=true - --providers.docker.exposedbydefault=false - --entrypoints.web.address=:80 - --entrypoints.websecure.address=:443 - --entrypoints.web.http.redirections.entrypoint.to=websecure - --entrypoints.web.http.redirections.entrypoint.scheme=https - --certificatesresolvers.letsencrypt.acme.tlschallenge=true - --certificatesresolvers.letsencrypt.acme.email=${CONTACT_EMAIL} - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json - --accesslog=true - --accesslog.filepath=/logs/access.log ports: - "80:80" - "443:443" volumes: - ./traefik/letsencrypt:/letsencrypt - ./traefik/logs:/logs - /var/run/docker.sock:/var/run/docker.sock:ro networks: - 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 - traefik.docker.network=fedeo_web 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 - traefik.docker.network=fedeo_web networks: - web networks: web: name: fedeo_web driver: bridge internal: name: fedeo_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 -f docker-compose.selfhost.yml build docker compose -f docker-compose.selfhost.yml up -d ``` Danach Status prufen: ```bash docker compose -f docker-compose.selfhost.yml ps docker compose -f docker-compose.selfhost.yml logs -f traefik docker compose -f docker-compose.selfhost.yml logs -f backend ``` Wenn du Migrationen manuell ausführen möchtest: ```bash docker compose -f docker-compose.selfhost.yml run --rm backend npm run migrate ``` ## 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"}` Wenn der Bootstrap aktiviert ist, kannst du dich danach mit `FEDEO_BOOTSTRAP_ADMIN_EMAIL` und `FEDEO_BOOTSTRAP_ADMIN_PASSWORD` anmelden. Die Mandantensperre wird über `locked` gesteuert; `hasActiveLicense` wird nicht mehr für den Selfhost-Zugriff ausgewertet. ## Updates Bei neuen Versionen: ```bash git pull docker compose -f docker-compose.selfhost.yml build docker compose -f docker-compose.selfhost.yml up -d ``` Der Backend-Container wendet Datenbankmigrationen beim Start automatisch an. Bei kritischen Updates sollte vorher ein Backup von `./postgres` und `./minio` erstellt werden. 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.