17 KiB
Deployment und Mailversand – Anleitung für den Kita-Planer
Stand: 09.10.2026. Grundlage sind die aktuelle Docker-/Coolify-Konfiguration und die umgesetzten Funktionen für Personal und Notdienstalarm.
1. Vor dem Deployment festlegen
- App-Adresse: eine feste HTTPS-Adresse, zum Beispiel
https://app.example.org. - Datenbank: entweder PostgreSQL aus dem mitgelieferten Compose-Stack oder eine bereits bestehende Datenbank.
- Versanddomain: eine eigene bei Resend verifizierte Domain, zum Beispiel
notify.example.org. - Absender: zum Beispiel
Kita-Planer <noreply@notify.example.org>. - Kontaktpostfach: eine tatsächlich erreichbare Adresse für
ADMIN_EMAIL. - Sicherung: Datenbank und Upload-Dateien vor einem Update sichern; die Wiederherstellung sollte bekannt sein.
Die Beispieladressen und Platzhalter in dieser Anleitung müssen durch deine Werte ersetzt werden. Zugangsdaten werden in der Hosting-Konfiguration hinterlegt und nicht ins Repository geschrieben.
2. Coolify und Datenbank richtig verbinden
In Coolify den gewünschten Release-Branch aus Gitea auswählen, Docker Compose als Build Pack und docker-compose.coolify.yml als Compose-Datei verwenden. Die aktuelle Implementierung des Notdienstalarms liegt auf codex/notdienstalarm; der ausgewählte Deployment-Stand muss diese Änderungen enthalten. Die öffentliche Domain dem Dienst app, internem Port 3000, zuordnen und HTTPS aktivieren.
Variante A: Datenbank des mitgelieferten Compose-Stacks
Die vorhandene Compose-Datei startet postgres, app und pgadmin. Für die Datenbank werden POSTGRES_DB, POSTGRES_USER und POSTGRES_PASSWORD gesetzt. Der App-Dienst erhält daraus automatisch:
postgresql://POSTGRES_USER:POSTGRES_PASSWORD@postgres:5432/POSTGRES_DB?schema=public
postgres ist hier der interne Dienstname. Eine zusätzlich in Coolify eingetragene Variable DATABASE_URL wird von der aktuellen Compose-Datei nicht verwendet, weil die URL dort direkt zusammengesetzt wird. Bei Sonderzeichen im Datenbankpasswort muss die URL korrekt kodiert sein; alternativ die Compose-Konfiguration auf eine ausdrücklich bereitgestellte, korrekt kodierte DATABASE_URL umstellen.
Variante B: Bereits vorhandene Coolify-PostgreSQL-Datenbank
Die Compose-Datei muss vor dem Deployment angepasst werden: Der App-Dienst soll die hinterlegte DATABASE_URL übernehmen, beispielsweise über DATABASE_URL: ${DATABASE_URL:?}. Die Abhängigkeit von dem bisherigen postgres-Dienst ist entsprechend anzupassen; einen unbenötigten separaten Datenbankdienst vermeiden. Falls pgAdmin weiter genutzt wird, dessen Konfiguration und Abhängigkeit ebenfalls prüfen.
Die App muss die bestehende Datenbank über das interne Coolify-Netzwerk erreichen können. localhost in der App-URL würde auf den App-Container zeigen. Vor dem Start verifizieren, dass die URL wirklich auf die gewünschte Datenbank zeigt; der Start wendet das Anwendungsschema auf genau diese Datenbank an.
3. Umgebungsvariablen
Die ausfüllbare Vorlage ist production.env.example. In Coolify werden Werte normalerweise ohne die Anführungszeichen einer .env-Datei eingetragen.
| Variable | Was du eintragen bzw. prüfen musst |
|---|---|
POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD |
Für den mitgelieferten Datenbankdienst. Bestehende Zugangsdaten bei Updates beibehalten; Werte mit der tatsächlichen Datenbank abgleichen. |
DATABASE_URL |
Bei einer angepassten Compose-Datei bzw. anderem Hosting: interne PostgreSQL-URL einschließlich Zugangsdaten und Schema. |
AUTH_SECRET |
Einmalig zufällig erzeugen und dauerhaft speichern; nicht bei jedem Deployment ersetzen. Ein Wechsel beendet bestehende Sessions. |
AUTH_URL |
Die öffentliche HTTPS-App-Adresse. |
NEXTAUTH_URL |
Dieselbe öffentliche Adresse; bestehende Einladungsabläufe verwenden auch diesen Namen. |
NEXT_PUBLIC_SITE_URL |
Dieselbe öffentliche Adresse, ohne abschließenden Slash und ohne Unterpfad. Für Mail-Links und öffentliche URLs erforderlich. |
RESEND_API_KEY |
Gültiger Resend-Schlüssel mit Versandberechtigung für die gewählte Domain. |
EMAIL_FROM |
Anzeigename und Adresse der verifizierten Versanddomain, beispielsweise Kita-Planer <noreply@notify.example.org>. |
ADMIN_EMAIL |
Empfänger der Kontaktformular-Anfragen. Diese Variable ersetzt nicht die Kita-Admin-Konten für Personal- oder Notdiensthinweise. |
NOTDIENST_JOB_SECRET |
Eigenes zufälliges Secret mit mindestens 32 Zeichen für die automatische Notdienst-Fristprüfung. |
UPLOAD_DIR |
Dauerhaft gespeichertes Verzeichnis für Anhänge; im aktuellen Compose-Stack /app/uploads/announcements. |
PGADMIN_DEFAULT_EMAIL, PGADMIN_DEFAULT_PASSWORD |
Nur erforderlich, wenn der mitgelieferte pgAdmin-Dienst genutzt wird. |
NEXT_PUBLIC_UMAMI_* |
Optional. Ohne Analytics leer lassen. |
Zwei getrennte Secrets lassen sich lokal erzeugen:
# AUTH_SECRET
openssl rand -base64 32
# NOTDIENST_JOB_SECRET – einen anderen Wert verwenden
openssl rand -hex 32
Die ausgegebenen Werte sicher in Coolify hinterlegen. NEXT_PUBLIC_SITE_URL muss im aktuellen Docker-Ablauf sowohl beim Build als auch zur Laufzeit vorhanden sein. Nach einer Domainänderung neu bauen und deployen. Nach Änderungen an Mail-Schlüssel oder Absender die App neu starten, damit die neue Konfiguration verwendet wird.
Für Produktion gilt NODE_ENV=production. TEST_MODE und ALLOW_DATABASE_SEED bleiben ungesetzt. Der vorhandene Befehl npm run dev führt einen Schemaabgleich und Demo-Seeding aus und ist deshalb kein Produktionsstart.
4. Schema, Start und dauerhafte Dateien
Der Docker-Build installiert Abhängigkeiten, generiert den Prisma-Client und erstellt den Produktionsbuild. Beim Container-Start führt scripts/start-production.sh aus:
npx prisma db push --skip-generate
Danach startet es den Notdienst-Worker und die App. Das Projekt besitzt weiterhin keine vollständige versionierte Prisma-Migrationshistorie. Vor Updates Schemaänderungen und Startlogs prüfen; bei einem fehlgeschlagenen Schemaabgleich die Ursache klären, bevor das Deployment fortgesetzt wird.
Für kontrollierte, einmalige SQL-Upgrades gibt es diese Alternativen:
| Ausgangsstand | Passendes SQL-Upgrade |
|---|---|
| Personalbereich noch nicht vorhanden, Stand passend zur Einführung | personalbereich.sql führt den vollständigen Personalbereich einschließlich Mail-Erweiterung ein. |
Personalbereich bereits auf 043fed2, Mail-Erweiterung noch nicht vorhanden |
Nur personalbereich-email.sql. |
| Personalbereich einschließlich Mail-Erweiterung vorhanden, Notdienstalarm noch nicht vorhanden | notdienstalarm.sql. |
personalbereich.sql und personalbereich-email.sql sind keine aufeinanderfolgende Skriptfolge: Das erste enthält die Mail-Erweiterung bereits. Die SQL-Skripte sind nicht für wiederholte Ausführung gedacht. Hat db push die betreffende Erweiterung schon angewandt, das entsprechende SQL nicht zusätzlich ausführen.
Im Compose-Stack sichern die Volumes kita_planer_postgres_data und kita_planer_uploads Datenbank und Anhänge über Containerwechsel hinweg. Bei Nutzung von pgAdmin kommt dessen eigenes Volume hinzu. Die Anwendung hängt announcements an UPLOAD_DIR an; im aktuellen Compose-Setup liegen Dateien deshalb unter /app/uploads/announcements/announcements. Dieser Pfad ist vom Volume /app/uploads abgedeckt. Bei einem Wechsel des Speicherpfads bestehende Dateien und Sicherungen berücksichtigen.
Bei anderem Hosting sind Build, Schemaabgleich, App-Start und Worker separat sicherzustellen. npm run start bzw. npm run start:prod allein starten nur die App, wenden kein Schema an und starten keinen Notdienst-Worker.
5. Mailversand mit Resend einrichten
Die Anwendung versendet über die Resend-API. SMTP-Host, SMTP-Port oder ein normales Postfachpasswort werden dafür nicht verwendet.
- Im Resend-Dashboard eine eigene Versanddomain hinzufügen. Eine Subdomain wie
notify.example.orghält den Transaktionsversand übersichtlich getrennt; auch Resend empfiehlt Subdomains für den Versand. - Die von Resend angezeigten DNS-Einträge für SPF/DKIM einschließlich der dort verlangten TXT-, MX- oder CNAME-Einträge exakt beim DNS-Anbieter übernehmen. Resend-CNAMEs als DNS-Einträge ohne Proxy konfigurieren. Keine generischen DNS-Werte aus dieser Anleitung einsetzen.
- Im Resend-Dashboard die erfolgreiche Verifizierung abwarten.
EMAIL_FROMauf eine Adresse genau dieser Versanddomain setzen. Bei Verifizierung vonnotify.example.orgbeispielsweisenoreply@notify.example.orgverwenden.
Die aktuellen Schritte beschreibt die offizielle Resend-Anleitung zur Domain-Verifizierung.
- DMARC passend zum bestehenden Mailbetrieb konfigurieren. Die Resend-DMARC-Anleitung erklärt die Einrichtung; bestehende Postfach- und DNS-Konfiguration berücksichtigen.
- Einen Versand-API-Schlüssel anlegen,
RESEND_API_KEYin Coolify setzen und die App neu starten. - Für Einladungs- und Rückmeldelinks Open-/Click-Tracking ausgeschaltet lassen. Das ist unsere Betriebsempfehlung für diese Transaktionsmails. Tracking ist bei Resend standardmäßig deaktiviert und kann Domains separat zugeordnet werden. Siehe Resend: Open and Click Tracking.
onboarding@resend.dev ist für den regulären Versand an Familien ungeeignet: Mit der Testdomain darf nur an die zum Resend-Konto gehörende Adresse gesendet werden. Für andere Empfänger braucht es die eigene verifizierte Domain. Siehe Resend: Testdomain und 403-Fehler.
ADMIN_EMAIL sollte ein betreutes Postfach sein. Für andere Absenderadressen separat entscheiden, ob Antworten empfangen werden sollen; die Versandkonfiguration allein richtet in der Anwendung kein betreutes Postfach ein. Im Kontaktformular setzt die App Reply-To für Anfrage und Eingangsbestätigung. Die aktuellen Personal- und Notdienstmails setzen kein eigenes Reply-To und führen die Empfänger über die Links in die App.
In Entwicklung ohne API-Schlüssel simuliert der Mailtransport den Versand und meldet Erfolg. Ein funktionierender lokaler Ablauf belegt daher noch keine echte Zustellung. In Produktion bleibt fehlgeschlagener Versand als Fehler bzw. ausstehender Auftrag sichtbar. Mit einem vorhandenen echten API-Schlüssel kann auch die Entwicklungsumgebung echte E-Mails verschicken.
6. Wer erhält welche E-Mails?
| Anlass | Empfänger / Verhalten |
|---|---|
| Kontaktformular | ADMIN_EMAIL erhält die Anfrage; die anfragende Person erhält eine Eingangsbestätigung. |
| Eltern-Einladung | Hinterlegte Elternadresse mit persönlichem Einladungslink. |
| Fachkraft aktivieren | Hinterlegte Fachkraftadresse; der Aktivierungslink gilt sieben Tage. Ein neuer Link ersetzt den alten. |
| Personal-Antrag / eingereichte Arbeitswoche | Alle zuständigen Kita-Admins derselben Kita. |
| Personal-Entscheidung | Betroffene Fachkraft, sofern Entscheidungsmails im Profil eingeschaltet sind. Aktivierungsmails bleiben davon unabhängig. |
| Notdienstalarm auslösen | Elternkonten der eingeteilten Familie; jedes Konto erhält einen eigenen Link. |
| Notdienst-Rückmeldung / Absage / Fristablauf | Kita-Admins und Notdienst-Koordination derselben Kita. |
| Notdienst-Stornierung / dokumentierte Statusänderung | Betroffener Haushalt; bei dokumentierter Rückmeldung erhalten auch Admins und Koordination einen Hinweis. |
Private Personalnotizen, Kontostände und interne Begründungen sowie interne Notdienst-Fachkraftdaten werden nicht in diese Personal-/Notdienstmails übernommen. Die Auswahl der Kita-Empfänger erfolgt anhand der Konten und Rollen in der Datenbank; ADMIN_EMAIL ist hierfür kein Sammelverteiler.
7. Automatische Notdienst-Fristprüfung und Wiederholungen
Mit gültigem NOTDIENST_JOB_SECRET startet der Docker-Produktionsstart scripts/notdienst-alarm-worker.mjs. Dieser ruft nach fünf Sekunden und danach ungefähr jede Minute die lokale Route POST /api/jobs/notdienstalarme auf. Die voreingestellte Antwortfrist beträgt 30 Minuten; die Meldung erfolgt beim nächsten erfolgreichen Joblauf nach Fristende, nicht sekundengenau.
Bei anderem Hosting den Worker dauerhaft und mit automatischem Neustart betreiben oder einen Scheduler mindestens einmal pro Minute die Jobroute per POST mit Authorization: Bearer <NOTDIENST_JOB_SECRET> aufrufen lassen. Der Prozess darf nicht davon abhängen, dass ein Browser geöffnet ist. Ein gesetztes Secret allein beweist keinen laufenden Worker.
| Bereich | Was passiert nach einem Versandfehler? |
|---|---|
| Notdienstalarm | Auftrag bleibt erhalten; der Job versucht den Versand erneut. In der Alarmansicht ist zusätzlich ein manueller erneuter Versuch möglich. Die Antwortfrist startet dabei nicht neu. |
| Personalbereich | Auftrag bleibt erhalten; über Versand erneut versuchen in der Oberfläche wiederholen. Aktuell gibt es hierfür keinen eigenen Hintergrundjob; der Notdienst-Worker bearbeitet keine Personalmails. |
| Andere bestehende Mailabläufe | Nicht alle besitzen eine dauerhafte Versandwarteschlange. Den konkreten Fehler und den Stand von Einladung/Aktion prüfen, bevor der Ablauf erneut ausgelöst wird. |
Ein Alarm-Job verarbeitet höchstens 100 Fristfälle und 100 ausstehende E-Mail-Aufträge pro Lauf. Bei größerem Rückstand sind weitere Läufe nötig. Die gespeicherte Aktion bleibt bei einem Mailfehler erhalten. Resend-Annahme bedeutet nicht, dass die Nachricht bereits im Postfach angekommen ist; eine automatische Auswertung von Zustell-/Bounce-Webhooks ist im geprüften Anwendungscode nicht angebunden.
8. Nach dem Deployment prüfen
Die Mailprüfung mit eigens dafür eingerichteten Konten und Postfächern durchführen. Bei einer Notdienstprüfung eine Test-Kita und eine Test-Einteilung verwenden, damit keine regulären Familien versehentlich alarmiert werden.
- HTTPS, Startseite, Anmeldung und Dashboard erreichbar.
- Build- und Startlogs ohne Schemafehler; App verwendet die richtige Datenbank.
- Anhänge nach einem Containerneustart weiterhin verfügbar.
- Kontaktformular: Anfrage kommt bei
ADMIN_EMAILan und Eingangsbestätigung im Testpostfach. - Eltern- und Fachkrafteinladung: korrekte öffentliche Domain im Link; Aktivierung funktioniert.
- Personal-Antrag: Kita-Admins erhalten den Hinweis; Entscheidung erreicht die Fachkraft. Abschalteinstellung im Profil geprüft.
- Test-Notdienstalarm: nur die eingeteilte Testfamilie erhält die Anfrage; eine Haushaltsantwort aktualisiert den gemeinsamen Status.
- Test-Fristablauf bei geschlossenem Browser: genau ein Hinweis pro zuständigem Admin/Koordinator, beim nächsten Joblauf nach Fristende.
- Stornierung informiert den Testhaushalt; alter Link kann keine Antwort mehr speichern.
- Resend-Dashboard und echte Postfächer einschließlich Spamordner geprüft; keine ausstehenden fehlgeschlagenen Aufträge übersehen.
- Resend-Kontingente, API-Fehler und Bounces im laufenden Betrieb überwacht.
9. Häufige Fehler
| Beobachtung | Zuerst prüfen |
|---|---|
| Mail fehlt trotz lokaler Erfolgsmeldung | War es Entwicklungsmodus ohne API-Schlüssel? Dann wurde nur simuliert. |
| Link zeigt auf localhost oder die falsche Domain | Alle drei App-URLs angleichen, NEXT_PUBLIC_SITE_URL auch beim Build setzen und neu deployen. Schon versendete Links ändern sich dadurch nicht rückwirkend. |
| Resend lehnt Versand ab | API-Schlüssel, verifizierte Domain, EMAIL_FROM, Versandberechtigung und Kontingente prüfen. Bei 403 auch die Testdomain prüfen. |
| Resend hat angenommen, Postfach bleibt leer | Resend-Zustellstatus, Bounce/Suppression, Empfängeradresse und Spamordner prüfen. |
| Personalaktion gespeichert, Mail ausstehend | Konfiguration beheben, dann im Personalbereich den Versand erneut versuchen. |
| Notdienst-Frist ist abgelaufen, kein Hinweis kommt | Worker/Scheduler, Logs und NOTDIENST_JOB_SECRET prüfen; Browseranzeige und tatsächliche Jobausführung sind getrennt. |
| Jobroute liefert 401 | Bearer-Header fehlt oder Secret stimmt nicht. |
| Jobroute liefert 503 | Secret fehlt oder ist kürzer als 32 Zeichen. |
| Jobroute liefert 500 | Joblauf fehlgeschlagen; Serverlogs und Datenbank prüfen. |
| Jobroute liefert 200, dennoch fehlen Mails | Das Ergebnis kann fehlgeschlagene Versandaufträge enthalten. Versandübersicht und Resend-Konfiguration prüfen. |
| Deployment startet nicht | Required-Variablen der Compose-Datei, Datenbankzugriff, Schemaabgleich und Secret-Länge prüfen. |
Ergänzende Details: Notdienstalarm, Personalbereich, Produktionscheckliste. Die produktive Hosting- und Resend-Konfiguration wurde für diese Anleitung nicht verändert; sie dokumentiert die noch auszuführenden Einrichtungsschritte.