diff --git a/README.md b/README.md index 870f8f0..698c298 100644 --- a/README.md +++ b/README.md @@ -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 -- PDF_LICENSE -- DB_PASS -- DB_USER -- CONTACT_EMAIL +Die Konfiguration erfolgt uber Umgebungsvariablen beziehungsweise eine `.env`-Datei im Deploy-Verzeichnis. -## 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 -> ` + +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 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.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 + +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: - 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: - image: traefik:v2.11 - restart: unless-stopped - container_name: traefik - command: - - "--api.insecure=false" - - "--api.dashboard=false" - - "--api.debug=false" - - "--providers.docker=true" - - "--providers.docker.exposedbydefault=false" - - "--providers.docker.network=traefik" - - "--entrypoints.web.address=:80" - - "--entrypoints.web-secured.address=:443" - - "--accesslog=true" - - "--accesslog.filepath=/logs/access.log" - - "--accesslog.bufferingsize=5000" - - "--accesslog.fields.defaultMode=keep" - - "--accesslog.fields.headers.defaultMode=keep" - - "--certificatesresolvers.mytlschallenge.acme.tlschallenge=true" # - - "--certificatesresolvers.mytlschallenge.acme.email=${CONTACT_EMAIL}" - - "--certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json" - ports: - - 80:80 - - 443:443 - volumes: - - "./traefik/letsencrypt:/letsencrypt" # <== Volume for certs (TLS) - - "/var/run/docker.sock:/var/run/docker.sock:ro" - - "./traefik/logs:/logs" - networks: - - traefik + 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 + 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: - traefik: - external: false -~~~ \ No newline at end of file + web: + 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.