KI-AGENT: Selfhosting für Secrets, Compose und Migrationen vorbereiten

This commit is contained in:
2026-05-18 18:06:03 +02:00
parent 571c24f250
commit 8824b1c9c8
6 changed files with 322 additions and 15 deletions

View File

@@ -1,3 +1,52 @@
# FEDEO Selfhosting
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
FEDEO_RUN_MIGRATIONS=true
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
# FEDEO Matrix-Kommunikation # FEDEO Matrix-Kommunikation
# #
# Diese Werte werden von docker-compose.yml gelesen, wenn das Profil "matrix" # Diese Werte werden von docker-compose.yml gelesen, wenn das Profil "matrix"

View File

@@ -89,7 +89,7 @@ Wenn du MinIO verwendest, setze zusatzlich:
## Deploy-Struktur ## Deploy-Struktur
Deploye den Stack direkt aus einem geklonten Checkout dieses Repositories, weil die Compose-Datei die lokalen Build-Kontexte `./frontend` und `./backend` verwendet. Deploye den Stack direkt aus einem geklonten Checkout dieses Repositories, weil die Selfhost-Compose-Datei die lokalen Build-Kontexte `./frontend` und `./backend` verwendet.
Beispiel: Beispiel:
@@ -102,7 +102,7 @@ Die Verzeichnisstruktur sollte dann mindestens so aussehen:
```text ```text
/opt/fedeo/ /opt/fedeo/
docker-compose.yml docker-compose.selfhost.yml
.env .env
backend/ backend/
frontend/ frontend/
@@ -124,6 +124,14 @@ touch /opt/fedeo/traefik/letsencrypt/acme.json
chmod 600 /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` ## Beispiel `.env`
Diese Datei liegt neben der `docker-compose.yml`: Diese Datei liegt neben der `docker-compose.yml`:
@@ -178,9 +186,11 @@ STIRLING_API_KEY=replace-this
NUXT_PUBLIC_PDF_LICENSE=replace-with-your-pdf-license NUXT_PUBLIC_PDF_LICENSE=replace-with-your-pdf-license
``` ```
## Vollstandiges Docker Compose mit optionaler S3-MinIO-Option ## 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. 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 ```yaml
services: services:
@@ -372,16 +382,22 @@ Hinweis: Das Backend nutzt `forcePathStyle: true`. Das funktioniert sauber mit M
Im Deploy-Verzeichnis: Im Deploy-Verzeichnis:
```bash ```bash
docker compose build docker compose -f docker-compose.selfhost.yml build
docker compose up -d docker compose -f docker-compose.selfhost.yml up -d
``` ```
Danach Status prufen: Danach Status prufen:
```bash ```bash
docker compose ps docker compose -f docker-compose.selfhost.yml ps
docker compose logs -f traefik docker compose -f docker-compose.selfhost.yml logs -f traefik
docker compose logs -f backend 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 ## Funktionsprufung
@@ -404,10 +420,12 @@ Bei neuen Versionen:
```bash ```bash
git pull git pull
docker compose build docker compose -f docker-compose.selfhost.yml build
docker compose up -d 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. 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 ## Backup-Empfehlung

View File

@@ -24,5 +24,5 @@ RUN npm run build
# Port freigeben # Port freigeben
EXPOSE 3100 EXPOSE 3100
# Start der App # Migrationen ausführen und App starten
CMD ["node", "dist/src/index.js"] CMD ["sh", "./docker-entrypoint.sh"]

View File

@@ -0,0 +1,7 @@
set -e
if [ "${FEDEO_RUN_MIGRATIONS:-true}" = "true" ]; then
npm run migrate
fi
exec node dist/src/index.js

View File

@@ -44,7 +44,75 @@ export let secrets = {
MATRIX_SERVICE_USER_LOCALPART?: string MATRIX_SERVICE_USER_LOCALPART?: string
} }
const secretKeys = [
"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",
"MATRIX_HOMESERVER_URL",
"MATRIX_SERVER_NAME",
"MATRIX_REGISTRATION_SHARED_SECRET",
"MATRIX_SERVICE_USER_LOCALPART",
] as const
const numberKeys = new Set(["PORT", "MAILER_SMTP_PORT", "DOKUBOX_IMAP_PORT"])
const booleanKeys = new Set(["DOKUBOX_IMAP_SECURE"])
function normalizeEnvValue(key: string, value: string) {
if (numberKeys.has(key)) return Number(value)
if (booleanKeys.has(key)) return value === "true"
return value
}
function loadSecretsFromEnv() {
let loaded = 0
secretKeys.forEach((key) => {
const value = process.env[key]
if (value === undefined || value === "") return
;(secrets as Record<string, any>)[key] = normalizeEnvValue(key, value)
loaded++
})
if (!secrets.HOST) secrets.HOST = "0.0.0.0"
if (!secrets.PORT) secrets.PORT = 3100
return loaded
}
export async function loadSecrets () { export async function loadSecrets () {
const envSecretCount = loadSecretsFromEnv()
if (!process.env.INFISICAL_CLIENT_ID || !process.env.INFISICAL_CLIENT_SECRET) {
console.log(`✅ Secrets aus Umgebungsvariablen geladen (${envSecretCount} Stück)`)
return
}
await client.auth().universalAuth.login({ await client.auth().universalAuth.login({
clientId: process.env.INFISICAL_CLIENT_ID, clientId: process.env.INFISICAL_CLIENT_ID,
@@ -57,8 +125,9 @@ export async function loadSecrets () {
}); });
allSecrets.secrets.forEach(secret => { allSecrets.secrets.forEach(secret => {
secrets[secret.secretKey] = secret.secretValue ;(secrets as Record<string, any>)[secret.secretKey] = normalizeEnvValue(secret.secretKey, secret.secretValue)
}) })
console.log("✅ Secrets aus Infisical geladen"); loadSecretsFromEnv()
console.log("✅ Secrets aus Infisical und Umgebungsvariablen geladen");
console.log(Object.keys(secrets).length + " Stück") console.log(Object.keys(secrets).length + " Stück")
} }

164
docker-compose.selfhost.yml Normal file
View File

@@ -0,0 +1,164 @@
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
FEDEO_RUN_MIGRATIONS: ${FEDEO_RUN_MIGRATIONS:-true}
HOST: ${HOST:-0.0.0.0}
PORT: ${PORT:-3100}
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}
NUXT_PUBLIC_MATRIX_ELEMENT_URL: ${NUXT_PUBLIC_MATRIX_ELEMENT_URL:-}
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:
web:
driver: bridge
internal:
driver: bridge