services: # Die Datenbank läuft jetzt hier statt bei Supabase. # # Was das Schema von der Plattform brauchte, bringt deploy/db-init mit: # die Anwendungsrolle ohne BYPASSRLS, zwei Erweiterungen und eine Attrappe # des `auth`-Schemas für die frühen Migrationen. Nachgewiesen ist, dass # alle Migrationen auf einer leeren Datenbank durchlaufen und dabei # Spalte für Spalte dasselbe Schema ergeben wie bisher. db: image: postgres:17-alpine restart: unless-stopped environment: POSTGRES_DB: ${POSTGRES_DB:-alpenwerk} POSTGRES_USER: ${POSTGRES_USER:-postgres} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD fehlt in .env} # Vom Startskript gelesen; damit steht das Passwort der Anwendungsrolle # nicht in einer Datei im Repository. APP_DB_PASSWORD: ${APP_DB_PASSWORD:?APP_DB_PASSWORD fehlt in .env} # Ohne das legt initdb die Datenbank mit der Locale des Abbilds an; # Sortierung von Umlauten ist bei einer Mitarbeiterliste keine # Nebensache. LANG: de_AT.UTF-8 POSTGRES_INITDB_ARGS: "--locale-provider=icu --icu-locale=de-AT --encoding=UTF8" volumes: - db-daten:/var/lib/postgresql/data # Läuft nur beim allerersten Start, solange das Datenverzeichnis leer # ist. Spätere Änderungen wirken erst auf einem frischen Band. - ./deploy/db-init:/docker-entrypoint-initdb.d:ro # Bewusst **kein** `ports:`. Die Datenbank ist nur im Compose-Netz # erreichbar, nicht vom Host und erst recht nicht von aussen. Wer von # aussen heranmuss (Sicherung, Migration), tut das über `docker compose # exec db` — dann gibt es keinen offenen Port, den jemand vergisst. healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-alpenwerk}"] interval: 10s timeout: 5s retries: 10 start_period: 30s # ── Werkzeuge, die nicht mitlaufen ─────────────────────────────────── # # Beide stehen im Profil `tools` und starten deshalb bei `docker compose # up` **nicht** mit. Sie werden einzeln gerufen (`docker compose run --rm # migrate`) und sind der Grund, warum auf dem Host weder Node noch ein # psql installiert sein muss — nur Docker. # Spielt ausstehende Migrationen gegen den db-Container ein. migrate: profiles: ["tools"] image: node:22-alpine working_dir: /repo volumes: - .:/repo environment: # Der direkte Zugang: im Compose-Netz gibt es keinen Pooler, und # scripts/migrate.mjs erwartet ohnehin die unmittelbare Verbindung. MIGRATE_DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-alpenwerk} DATABASE_SSL: "false" depends_on: db: condition: service_healthy # `pg` liegt im Repository; fehlt node_modules auf dem Server, wird es # einmalig nachgeladen, ohne package.json anzufassen. entrypoint: - sh - -c - 'test -d node_modules/pg || npm install --no-save --no-audit --no-fund pg@8 >/dev/null; node scripts/migrate.mjs "$@"' - -- # psql und pg_dump, ohne sie auf dem Host zu installieren. psql: profiles: ["tools"] image: postgres:17-alpine volumes: - .:/repo working_dir: /repo environment: PGPASSWORD: ${POSTGRES_PASSWORD} depends_on: db: condition: service_healthy entrypoint: ["psql", "-v", "ON_ERROR_STOP=1", "-h", "db", "-U", "${POSTGRES_USER:-postgres}", "-d", "${POSTGRES_DB:-alpenwerk}"] app: # Ohne Build-Argumente: alles, was die Anwendung braucht — DATABASE_URL, # AUTH_* — liest sie zur Laufzeit aus .env. Das Abbild ist damit für jede # Umgebung dasselbe. build: context: . restart: unless-stopped depends_on: db: condition: service_healthy # Nur auf der Loopback-Adresse, nicht auf allen Schnittstellen. Erreichbar # ist die App damit ausschliesslich über den Reverse Proxy, der TLS # beendet — sonst stünde daneben derselbe Dienst unverschlüsselt offen, # und ein Fehler in der Firewall genügte. ports: - "127.0.0.1:3000:3000" env_file: - .env # Replaces the Vercel Cron job from vercel.json (not available outside # Vercel): calls the same endpoint on the same daily schedule using the # same bearer-secret auth the route already expects. cron: image: alpine:3.20 restart: unless-stopped depends_on: app: condition: service_healthy env_file: - .env entrypoint: ["/bin/sh", "-c"] command: - | echo "0 3 * * * /bin/sh -c 'wget -q -O- --header=\"Authorization: Bearer \$$CRON_SECRET\" http://app:3000/api/cron/apply-pending-changes >> /var/log/cron.log 2>&1'" > /etc/crontabs/root crond -f -d 8 volumes: # Benannt, nicht als Pfad auf dem Host: `docker compose down` lässt es # stehen, und nur ein ausdrückliches `down -v` löscht es. Der Unterschied # zwischen „Dienst neu gestartet" und „Personaldaten weg". db-daten: