diff --git a/push-server/.env.example b/push-server/.env.example index e0991dc..ae65f94 100644 --- a/push-server/.env.example +++ b/push-server/.env.example @@ -1,10 +1,14 @@ NODE_ENV=development +CENTRAL_API_DOMAIN=services.fedeo.de +CENTRAL_ADMIN_DOMAIN=services-admin.fedeo.de +CONTACT_EMAIL=admin@example.com +POSTGRES_PASSWORD=change-me-postgres-password DATABASE_URL=postgres://fedeo_push:fedeo_push@localhost:5442/fedeo_push API_HOST=0.0.0.0 API_PORT=4020 ADMIN_TOKEN=change-me-admin-token PUSH_SECRET_ENCRYPTION_KEY=change-me-32-byte-minimum-secret -PUBLIC_BASE_URL=http://localhost:4020 +PUBLIC_BASE_URL=https://services.fedeo.de WEB_PUSH_PUBLIC_KEY= WEB_PUSH_PRIVATE_KEY= IOS_BUNDLE_ID=software.federspiel.fedeo @@ -22,5 +26,5 @@ OPENAI_BASE_URL=https://api.openai.com/v1 GOCARDLESS_BASE_URL=https://bankaccountdata.gocardless.com/api/v2 GOCARDLESS_SECRET_ID= GOCARDLESS_SECRET_KEY= -NUXT_PUBLIC_API_BASE=http://localhost:4020 +NUXT_PUBLIC_API_BASE=https://services.fedeo.de NUXT_ADMIN_TOKEN=change-me-admin-token diff --git a/push-server/.gitignore b/push-server/.gitignore index 66892f4..c71a46e 100644 --- a/push-server/.gitignore +++ b/push-server/.gitignore @@ -5,3 +5,4 @@ dist .env .env.local *.log +traefik/letsencrypt/acme.json diff --git a/push-server/README.md b/push-server/README.md index 2f73b74..744c2a3 100644 --- a/push-server/README.md +++ b/push-server/README.md @@ -7,7 +7,7 @@ Eigenständiger Stack für zentrale FEDEO-Dienste. Neben Push können Selfhost-I - `apps/api`: Fastify API für Admin-Dashboard und Selfhost-Instanzen - `apps/admin`: Nuxt Admin Dashboard mit Nuxt UI - `packages/db`: Drizzle Schema und Migrationen für PostgreSQL -- `docker-compose.yml`: lokaler Stack aus Postgres, API und Admin +- `docker-compose.yml`: produktiver Stack aus Traefik, Postgres, API und Admin Provider-Schlüssel wie `OPENAI_API_KEY` und die GoCardless-Zugangsdaten liegen ausschließlich in diesem zentralen Stack. Selfhost-Instanzen erhalten nur Instanz-ID und Instanzschlüssel. @@ -22,22 +22,48 @@ npm run dev:api npm run dev:admin ``` -Für den dauerhaften Betrieb: - -```bash -cp .env.example .env -# .env mit sicheren Zugangsdaten und Provider-Schlüsseln befüllen -docker compose up -d --build -``` - -Der API-Container wendet Datenbankmigrationen vor jedem Start automatisch an. PostgreSQL-Daten liegen im Volume `push_postgres_data`. Für ein öffentliches Deployment müssen API und Admin hinter einem TLS-Reverse-Proxy betrieben und `PUBLIC_API_BASE_URL` auf die öffentliche API-Adresse gesetzt werden. - -Standardports: +Lokale Entwicklungsports: - API: `http://localhost:4020` -- Admin: `http://localhost:3000` lokal oder `http://localhost:3020` über Docker Compose +- Admin: `http://localhost:3000` - Postgres: `localhost:5442` +## Öffentlicher Betrieb mit Traefik + +Voraussetzungen: + +- Docker Engine mit Docker-Compose-Plugin +- freie Ports `80` und `443` +- A- beziehungsweise AAAA-Records für API- und Admin-Domain auf den Server + +Der geführte Erststart erzeugt sichere lokale Schlüssel, bereitet den Let’s-Encrypt-Speicher vor und kann den Stack direkt starten: + +```bash +cd push-server +chmod +x scripts/setup.sh +./scripts/setup.sh --start +``` + +Standardmäßig werden `services.fedeo.de` für die API und `services-admin.fedeo.de` für das Admin-Dashboard vorgeschlagen. Anschließend müssen die zentralen Provider-Zugangsdaten in `.env` gepflegt werden: + +```env +OPENAI_API_KEY=... +GOCARDLESS_SECRET_ID=... +GOCARDLESS_SECRET_KEY=... +``` + +Ein Neustart nach Änderungen erfolgt mit: + +```bash +docker compose up -d +docker compose ps +docker compose logs -f api +``` + +Traefik leitet HTTP automatisch auf HTTPS um und bezieht Zertifikate per Let’s Encrypt TLS-Challenge. PostgreSQL besitzt keinen Host-Port und ist nur im internen Docker-Netz erreichbar. API und Admin werden ebenfalls nicht direkt über Host-Ports veröffentlicht. + +Der API-Container wendet Datenbankmigrationen vor jedem Start automatisch an. PostgreSQL-Daten liegen im Volume `push_postgres_data`; Let’s-Encrypt-Daten liegen unter `traefik/letsencrypt`. + ## Admin-Zugang Das Dashboard nutzt den Wert aus `ADMIN_TOKEN`. Der Token wird im Browser nur lokal gespeichert und als `Authorization: Bearer ...` an die API gesendet. diff --git a/push-server/docker-compose.yml b/push-server/docker-compose.yml index f573527..170a691 100644 --- a/push-server/docker-compose.yml +++ b/push-server/docker-compose.yml @@ -1,12 +1,39 @@ services: + traefik: + image: traefik:v3.3 + restart: unless-stopped + command: + - --providers.docker=true + - --providers.docker.exposedbydefault=false + - --entrypoints.web.address=:80 + - --entrypoints.web.http.redirections.entrypoint.to=websecure + - --entrypoints.web.http.redirections.entrypoint.scheme=https + - --entrypoints.websecure.address=:443 + - --certificatesresolvers.letsencrypt.acme.tlschallenge=true + - --certificatesresolvers.letsencrypt.acme.email=${CONTACT_EMAIL} + - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json + - --ping=true + ports: + - "80:80" + - "443:443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./traefik/letsencrypt:/letsencrypt + healthcheck: + test: ["CMD", "traefik", "healthcheck", "--ping"] + interval: 15s + timeout: 5s + retries: 5 + networks: + - web + postgres: image: postgres:17 + restart: unless-stopped environment: POSTGRES_DB: fedeo_push POSTGRES_USER: fedeo_push POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-fedeo_push} - ports: - - "5442:5432" volumes: - push_postgres_data:/var/lib/postgresql/data healthcheck: @@ -14,6 +41,8 @@ services: interval: 10s timeout: 5s retries: 5 + networks: + - internal api: restart: unless-stopped @@ -23,8 +52,6 @@ services: env_file: ${CENTRAL_ENV_FILE:-.env} environment: DATABASE_URL: postgres://fedeo_push:${POSTGRES_PASSWORD:-fedeo_push}@postgres:5432/fedeo_push - ports: - - "4020:4020" depends_on: postgres: condition: service_healthy @@ -33,6 +60,16 @@ services: interval: 15s timeout: 5s retries: 5 + labels: + - traefik.enable=true + - traefik.http.routers.fedeo-central-api.rule=Host(`${CENTRAL_API_DOMAIN}`) + - traefik.http.routers.fedeo-central-api.entrypoints=websecure + - traefik.http.routers.fedeo-central-api.tls.certresolver=letsencrypt + - traefik.http.services.fedeo-central-api.loadbalancer.server.port=4020 + - traefik.docker.network=fedeo_central_web + networks: + - web + - internal admin: restart: unless-stopped @@ -41,12 +78,26 @@ services: dockerfile: apps/admin/Dockerfile env_file: ${CENTRAL_ENV_FILE:-.env} environment: - NUXT_PUBLIC_API_BASE: ${PUBLIC_API_BASE_URL:-http://localhost:4020} - ports: - - "3020:3000" + NUXT_PUBLIC_API_BASE: https://${CENTRAL_API_DOMAIN} depends_on: api: condition: service_healthy + labels: + - traefik.enable=true + - traefik.http.routers.fedeo-central-admin.rule=Host(`${CENTRAL_ADMIN_DOMAIN}`) + - traefik.http.routers.fedeo-central-admin.entrypoints=websecure + - traefik.http.routers.fedeo-central-admin.tls.certresolver=letsencrypt + - traefik.http.services.fedeo-central-admin.loadbalancer.server.port=3000 + - traefik.docker.network=fedeo_central_web + networks: + - web volumes: push_postgres_data: + +networks: + web: + name: fedeo_central_web + internal: + name: fedeo_central_internal + internal: true diff --git a/push-server/scripts/setup.sh b/push-server/scripts/setup.sh new file mode 100755 index 0000000..3c89fef --- /dev/null +++ b/push-server/scripts/setup.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ENV_FILE="$ROOT_DIR/.env" +ENV_EXAMPLE="$ROOT_DIR/.env.example" + +prompt() { + local label="$1" + local default_value="$2" + local value + read -r -p "$label [$default_value]: " value + printf '%s' "${value:-$default_value}" +} + +replace_env() { + local key="$1" + local value="$2" + local escaped + escaped="${value//\\/\\\\}" + escaped="${escaped//&/\\&}" + escaped="${escaped//|/\\|}" + if grep -q "^${key}=" "$ENV_FILE"; then + sed -i "s|^${key}=.*$|${key}=${escaped}|" "$ENV_FILE" + else + printf '%s=%s\n' "$key" "$value" >> "$ENV_FILE" + fi +} + +random_secret() { + openssl rand -hex "$1" +} + +command -v docker >/dev/null 2>&1 || { echo "Docker ist nicht installiert." >&2; exit 1; } +docker compose version >/dev/null 2>&1 || { echo "Docker Compose Plugin ist nicht installiert." >&2; exit 1; } +command -v openssl >/dev/null 2>&1 || { echo "OpenSSL ist nicht installiert." >&2; exit 1; } + +if [[ ! -f "$ENV_FILE" ]]; then + cp "$ENV_EXAMPLE" "$ENV_FILE" + echo ".env wurde aus .env.example angelegt." +fi + +echo "FEDEO Central Services Setup" +echo +api_domain="$(prompt "API-Domain" "services.fedeo.de")" +admin_domain="$(prompt "Admin-Domain" "services-admin.fedeo.de")" +contact_email="$(prompt "E-Mail für Let's Encrypt" "admin@example.com")" + +replace_env "CENTRAL_API_DOMAIN" "$api_domain" +replace_env "CENTRAL_ADMIN_DOMAIN" "$admin_domain" +replace_env "CONTACT_EMAIL" "$contact_email" +replace_env "PUBLIC_BASE_URL" "https://$api_domain" +replace_env "NUXT_PUBLIC_API_BASE" "https://$api_domain" + +grep -q '^POSTGRES_PASSWORD=change-me-' "$ENV_FILE" && replace_env "POSTGRES_PASSWORD" "$(random_secret 24)" +grep -q '^ADMIN_TOKEN=change-me-' "$ENV_FILE" && replace_env "ADMIN_TOKEN" "$(random_secret 32)" +grep -q '^PUSH_SECRET_ENCRYPTION_KEY=change-me-' "$ENV_FILE" && replace_env "PUSH_SECRET_ENCRYPTION_KEY" "$(random_secret 32)" + +mkdir -p "$ROOT_DIR/traefik/letsencrypt" +touch "$ROOT_DIR/traefik/letsencrypt/acme.json" +chmod 600 "$ROOT_DIR/traefik/letsencrypt/acme.json" + +echo +echo "Konfiguration: $ENV_FILE" +echo "API: https://$api_domain" +echo "Admin: https://$admin_domain" +echo +echo "Bitte OPENAI_API_KEY sowie GOCARDLESS_SECRET_ID und GOCARDLESS_SECRET_KEY in .env ergänzen." +echo "Die DNS-A/AAAA-Records beider Domains müssen auf diesen Server zeigen." + +if [[ "${1:-}" == "--start" ]]; then + cd "$ROOT_DIR" + docker compose up -d --build + docker compose ps +else + echo "Start: cd $ROOT_DIR && docker compose up -d --build" +fi