# Deployment und Go-Live Stand: 2026-08-06 Dieses Dokument beschreibt, wie die Anwendung auf den Netcup-Webspace (Plesk) ausgerollt und in Betrieb genommen wird. Es ergänzt `docs/betrieb-backup-monitoring.md` (Backup/Monitoring) und `docs/betrieb-mail.md` (Mailversand). ## Umgebungen | Umgebung | Host | Webroot | Zweck | | --- | --- | --- | --- | | Staging | `testumgebung.kaffeeliste.de` | `/testumgebung.kaffeeliste.de/httpdocs/` | Vollständiger Durchlauf vor jedem Produktiv-Deploy | | Produktion | `app.kaffeeliste.de` (App) + `kaffeeliste.de` (Marketing) | noch einzurichten | Echtbetrieb | Beide Umgebungen brauchen **eigene Datenbanken** und **eigene** `env.local.php`. Die Staging-Umgebung läuft mit Stripe-Testkeys, die Produktion mit Live-Keys. ### Host-Split beachten `index.php` prüft den Request-Host gegen `APP_HOST`: stimmt er nicht überein, wird statt des Dashboards die Landingpage ausgeliefert. Auf Staging muss deshalb `APP_HOST=testumgebung.kaffeeliste.de` gesetzt sein, sonst kommt man nie ins Dashboard. In Produktion zeigen beide Vhosts (`kaffeeliste.de` und `app.kaffeeliste.de`) auf dasselbe Webroot; `APP_HOST=app.kaffeeliste.de` sorgt dann automatisch dafür, dass die Marketing-Domain die Landingpage und die App-Domain das Dashboard zeigt. ## Was deployt wird — und was nicht Der Upload läuft über den FTP-Sync (`sync_config.jsonc`, Umgebung `netcup`). Die Ausschlussliste dort ist sicherheitsrelevant und darf nicht geleert werden: Nicht ausliefern: `.git`, `.gitignore`, `.claude`, `.local`, `.vscode`, `.env.local`, `env.local.php`, `sync_config.jsonc`, `var/`, `docs/`, `README.md`. - `.git` wäre sonst komplett herunterladbar (kompletter Quellcode plus Historie). - `.env.local` enthält die Zugangsdaten der Entwicklungsdatenbank. - `sync_config.jsonc` enthält das FTP-Passwort im Klartext. - `var/` ist Laufzeitzustand des Servers (Sessions, Uploads) und würde vom lokalen Stand überschrieben. `scripts/` und `database/` **werden** mit ausgeliefert — die Plesk-Scheduled-Tasks führen sie vom Webspace aus. Per URL sind sie durch die `.htaccess` gesperrt. Zusätzlich sperrt die `.htaccess` `var|database|scripts|docs|app|lib`, die Vendor-Verzeichnisse `TCPDF|PHPMailer|DataTables`, Dotfile-Ordner sowie Konfigurationsdateien und `.sql`/`.md`/`.jsonc`/`.sh`/`.log`-Dateien. Sie erzwingt außerdem HTTPS — ohne Redirect käme ein Nutzer per `http` an und bekäme eine Session-Cookie ohne `secure`-Flag. ## Erstinbetriebnahme einer Umgebung ### 1. Hosting-Voraussetzungen prüfen In Plesk unter *PHP-Einstellungen* bzw. über eine temporäre `phpinfo()`-Datei kontrollieren: - PHP ≥ 8.1 - Extensions: `pdo_mysql`, `fileinfo` (CSV-Upload), `openssl`, `imap` (nur für den PayPal-Mailabruf nötig) - **`allow_url_fopen = On`** — `app/stripe.php` und `app/dolibarr.php` sprechen ihre APIs bewusst über PHP-Streams statt cURL an und fallen ohne diese Einstellung aus - `display_errors = Off`, `log_errors = On` (siehe `docs/betrieb-backup-monitoring.md`) - Let's-Encrypt-Zertifikat für den Host ausgestellt Die temporäre `phpinfo()`-Datei danach wieder löschen. ### 2. Datenbank anlegen In Plesk eine leere MySQL-Datenbank samt eigenem Benutzer anlegen. Die Zugangsdaten werden gleich in `env.local.php` eingetragen. Die vorhandene Entwicklungsdatenbank **nicht** übernehmen: sie enthält ausschließlich Testdaten inklusive des migrierten Default-Mandanten aus der Legacy-App. ### 3. Dateien hochladen FTP-Sync ausführen. Danach prüfen, dass folgende URLs **403/404** liefern und nicht etwa Inhalt: ``` https:///sync_config.jsonc https:///.env.local https:///.git/config https:///scripts/migrate.php https:///docs/deployment.md https:///app/bootstrap.php ``` Liefert eine davon Inhalt, greift die `.htaccess` nicht (z. B. weil `AllowOverride` deaktiviert ist) — dann erst weitermachen, wenn das geklärt ist. ### 4. `env.local.php` anlegen `env.local.example.php` als Vorlage nehmen, ausfüllen und **direkt per FTP** als `env.local.php` ins Webroot legen. Die Datei ist bewusst nicht im Repository und wird nicht mit deployt, damit ein Deploy sie nie überschreibt. Für Staging mindestens: ```php putenv('APP_ENV=prod'); // 'dev' würde Mails nur ins Log schreiben putenv('APP_HOST=testumgebung.kaffeeliste.de'); putenv('APP_TIMEZONE=Europe/Berlin'); putenv('APP_DB_DRIVER=mysql'); putenv('DB_HOST=localhost'); putenv('DB_NAME=…'); putenv('DB_USER=…'); putenv('DB_PASS=…'); putenv('APP_SESSION_PATH=' . __DIR__ . '/var/sessions'); putenv('APP_MAIL_TRANSPORT=mail'); putenv('APP_MAIL_FROM=noreply@kaffeeliste.de'); ``` Stripe-, Dolibarr- und IMAP-Werte je Umgebung ergänzen (siehe unten). ### 5. Schreibrechte für `var/` `app_start_session()` legt `var/sessions` bei Bedarf selbst an, braucht dafür aber Schreibrechte im Webroot. Nach dem ersten Aufruf prüfen, dass `var/sessions` existiert und beschreibbar ist; ebenso `var/uploads` für den CSV-Import. Beide sind per `.htaccess` von außen gesperrt. ### 6. Migrationen einspielen Es gibt 24 versionierte Migrationen in `database/migrations/`, angewendet über `scripts/migrate.php`. Das Skript ist idempotent und wendet nur fehlende Migrationen an. Ohne SSH-Zugang läuft das über **Plesk → Geplante Aufgaben (Scheduled Tasks)** als einmalig ausgeführter Task vom Typ *PHP-Skript ausführen*: ``` Skriptpfad: /httpdocs/scripts/migrate.php ``` Der Task muss nach dem Ausführen wieder deaktiviert oder gelöscht werden. Die Ausgabe (`Applied migration: …` bzw. `No new migrations.`) lässt sich per E-Mail-Benachrichtigung des Tasks kontrollieren. Alternative ohne Scheduled Task: die Migrationsdateien der Reihe nach in phpMyAdmin importieren — dann fehlt allerdings der Eintrag in der Versionstabelle, den `migrate.php` pflegt. Der Scheduled-Task-Weg ist deutlich vorzuziehen. **Nach jedem Deploy mit neuen Migrationen wiederholen.** ### 7. Ersten Mandanten anlegen Über `register.php` auf dem konfigurierten Host registrieren. Für Plattform-Administrationsrechte (Back-Office über alle Mandanten hinweg) anschließend `scripts/grant-platform-admin.php` als einmaligen Plesk-Task ausführen. ## Wiederkehrende Aufgaben (Plesk Scheduled Tasks) Ohne diese Tasks fehlen Zahlungserinnerungen, die automatische PayPal-Verbuchung und — kritisch — die Backups. | Task | Skript / Befehl | Empfohlener Takt | | --- | --- | --- | | Zahlungserinnerungen | `/httpdocs/scripts/send-payment-reminders.php` | täglich, z. B. 07:00 | | PayPal-Mailabruf | `/httpdocs/scripts/fetch-paypal-payments.php` | alle 15–30 Minuten | | Datenbank-Backup | Shell-Befehl, siehe `docs/betrieb-backup-monitoring.md` | täglich nachts | Zum Backup-Task: Das Zielverzeichnis muss **außerhalb** von `httpdocs` liegen (z. B. `/backups` auf Vhost-Ebene), sonst wären die Dumps öffentlich abrufbar. Zusätzlich mindestens eine Kopie an einen anderen Ort übertragen — ein Backup, das nur auf demselben Webspace liegt, schützt nicht gegen den Ausfall des Anbieters. Vor der Scharfschaltung beide PHP-Tasks einmal manuell auslösen. `send-payment-reminders.php` unterstützt dafür `--dry-run`. ## Mailversand und DNS Der Versand läuft über PHPs `mail()` (`APP_MAIL_TRANSPORT=mail`, siehe `docs/betrieb-mail.md`). Das betrifft Registrierungsbestätigung, E-Mail-Verifikation, Passwort-Reset, Mitglieder-Einladungen, Zahlungserinnerungen und den Massenmailversand — also den kompletten Onboarding-Pfad. Vor Go-Live für die Absenderdomain einrichten und prüfen: - **SPF**-Eintrag, der den Mailserver des Webspace autorisiert - **DKIM**-Signierung (in Plesk pro Domain aktivierbar) - **DMARC**-Eintrag - `APP_MAIL_FROM` auf eine echte Adresse der eigenen Domain setzen — eine fremde Absenderdomain bricht SPF/DKIM Testen mit einer Registrierung auf eine externe Adresse (Gmail/Outlook) und Kontrolle, ob die Mail im Posteingang statt im Spam landet. Fehlversuche stehen in der Tabelle `outbound_emails` mit `status = 'failed'`. ## Stripe und Dolibarr Details zur Funktionsweise stehen in `docs/billing.md`. Für den Go-Live: 1. **Staging** mit Stripe-**Test**-Keys betreiben und den kompletten Ablauf einmal durchspielen: Checkout, Upgrade, Downgrade, Kündigung, Customer Portal. 2. Webhook-Endpunkt im Stripe-Dashboard auf `https:///stripe-webhook.php` registrieren (je Umgebung einen eigenen) und das dort erzeugte Signing Secret als `STRIPE_WEBHOOK_SECRET` eintragen. Ohne gültiges Secret weist `stripe-webhook.php` alle Ereignisse ab. 3. Für Produktion auf **Live**-Keys wechseln und den Webhook erneut registrieren — Test- und Live-Modus haben getrennte Webhooks und getrennte Secrets. 4. Dolibarr: `DOLIBARR_URL` und `DOLIBARR_API_KEY` eintragen. Achtung, das ist die **produktive** Buchhaltungsinstanz des Kunden, es gibt keine Sandbox. Der `validate()`-Aufruf beim Rechnungsabschluss ist laut `docs/billing.md` bewusst nie live getestet worden — der erste echte Zahlungseingang ist dessen erster Test. Danach in Dolibarr kontrollieren, ob die Rechnung korrekt angelegt und validiert wurde. ## PayPal-Postfach Für die automatische Verbuchung weitergeleiteter PayPal-Zahlungsmails muss das Postfach aus `PAYPAL_INBOX_BASE` (Vorgabe `zahlungen@kaffeeliste.de`) existieren und **Plus-Adressierung an dieselbe Mailbox zustellen** (Catch-All), damit die pro Mandant generierten Adressen `zahlungen+@…` ankommen. IMAP-Zugangsdaten in `env.local.php` eintragen und die `imap`-Extension auf dem Host prüfen. ### Erster Testlauf `scripts/fetch-paypal-payments.php --dry-run` fasst nichts an: es meldet Verbindung, erkannten Mandanten und was mit jeder Mail passieren *würde* (`would_book` / `would_queue` / `would_park` / `duplicate`), speichert aber nichts, bucht nichts und markiert keine Mail als gelesen. Erst der Lauf ohne `--dry-run` verbucht. Reihenfolge für die Inbetriebnahme: 1. Eingangsadresse des Mandanten holen — sie steht auf `paypal-zuordnung.php` („PayPal-Zahlungen"). 2. Eine echte PayPal-Zahlungsmail dorthin weiterleiten. 3. Task mit `--dry-run` auslösen, Ausgabe kontrollieren. 4. Task ohne `--dry-run` auslösen, Ergebnis in der App prüfen (Journal-Vorschau bzw. Warteschlange). 5. Erst danach den Task auf den regelmäßigen Takt stellen. Ohne IMAP lässt sich die Verarbeitung auch mit einer gespeicherten Mail prüfen: ``` php scripts/fetch-paypal-payments.php --file=mail.html \ --recipient=zahlungen+@kaffeeliste.de --from=service@paypal.de [--dry-run] ``` Die Verarbeitungskette selbst (Absenderprüfung, Token, Parser, Zuordnung, Buchung, Dedup, Parken) deckt `scripts/check-paypal-inbox-flow.php` als Regressionstest ab — alles außer der IMAP-Verbindung. ## Deploy-Ablauf für ein Update 1. Lokal committen und pushen. 2. FTP-Sync ausführen (Ausschlussliste greift automatisch). 3. Falls neue Migrationen dabei sind: Migrations-Task in Plesk einmal auslösen. 4. Stichprobe: Login, Dashboard, eine Strich-Buchung, PDF-Export. Ein echtes Rollback gibt es nicht; im Fehlerfall wird der vorherige Commit-Stand erneut hochgeladen. Migrationen sind nicht rückwärts lauffähig — vor einem Deploy mit Schemaänderungen daher immer ein frisches Backup ziehen. ## Go-Live-Checkliste Vor der Umstellung auf `app.kaffeeliste.de`: - [ ] Kompletter Funktionsdurchlauf auf Staging erfolgreich (Registrierung, Mailversand, Mitglieder, Striche, Einzahlungen, CSV-Import, PDF-Export, Jahresauswertung, Stripe im Testmodus) - [ ] Die sechs Abruf-Tests aus Schritt 3 liefern alle 403/404 - [ ] `display_errors` produktiv aus, `log_errors` an - [ ] SPF/DKIM/DMARC gesetzt, Testmail landet nicht im Spam - [ ] Backup-Task läuft und ein Restore wurde einmal testweise eingespielt (`docs/betrieb-backup-monitoring.md`) - [ ] Stripe-Live-Keys und Produktiv-Webhook eingetragen - [ ] Impressum, AGB und Datenschutzerklärung inhaltlich freigegeben - [ ] Auftragsverarbeitungsvertrag (AVV) für Kunden vorbereitet - [ ] Frische Produktivdatenbank ohne Testdaten ## Bewusst offen - **Content-Security-Policy**: nicht gesetzt, weil die Templates durchgängig Inline-Styles nutzen. Begründung und Umfang in `docs/m8-haertung.md`. - **Automatisiertes Deployment** (CI/CD, Build-Schritt): nicht eingerichtet. Bei einer flachen PHP-App ohne Build-Prozess und einem einzelnen Deployer ist der FTP-Sync angemessen. - **`DataTables/` und `js/`**: liegen im Repo, werden aber von keiner Seite mehr eingebunden. Sie sind per `.htaccess` gesperrt und können bei Gelegenheit entfernt werden.