# Deployment und Go-Live Stand: 2026-08-22 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 | Tabellen-Prefix | Zweck | | --- | --- | --- | --- | --- | | Staging | `testumgebung.kaffeeliste.de` | `/testumgebung.kaffeeliste.de/httpdocs/` | `test_` | Vollständiger Durchlauf vor jedem Produktiv-Deploy | | Produktion | `app.kaffeeliste.de` (App) + `kaffeeliste.de` (Marketing) | noch einzurichten | `prod_` | Echtbetrieb | Beide Umgebungen verwenden dieselbe physische Datenbank, aber zwingend unterschiedliche Tabellen-Prefixe und jeweils eine eigene `env.local.php`. Damit sind Daten, Migrationstabellen und Fremdschlüssel von Staging und Produktion logisch getrennt. Die Staging-Umgebung läuft mit Stripe-Testkeys, die Produktion mit Live-Keys. Der Prefix schützt vor versehentlichem Zugriff der einen Umgebung auf die Tabellen der anderen, ersetzt aber keine getrennte Datenbank als harte Sicherheitsgrenze: Ein kompromittierter gemeinsamer Datenbankzugang kann weiterhin alle Tabellen sehen. Deshalb Zugangsdaten besonders restriktiv behandeln und immer die gesamte Datenbank sichern. ### 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. Im Ordner `legal/` sind ausschließlich datierte `.txt`-Fassungen von AGB, Datenschutz, AVV und Widerrufsbelehrung als dauerhafte Downloads erlaubt. 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 MySQL-Datenbank samt Benutzer anlegen. Beide Umgebungen dürfen dieselben Zugangsdaten verwenden; die Trennung erfolgt über `DB_TABLE_PREFIX=test_` beziehungsweise `DB_TABLE_PREFIX=prod_`. Unpräfixierte vorhandene Testdaten dürfen ausschließlich mit dem dafür vorgesehenen Übernahmeskript nach `test_` kopiert werden. Sie dürfen niemals nach `prod_` übernommen werden. ### 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('DB_TABLE_PREFIX=test_'); 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). In der produktiven `env.local.php` muss stattdessen `DB_TABLE_PREFIX=prod_` stehen. Ein leerer Prefix ist im laufenden Betrieb nicht zulässig. ### 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 30 versionierte Migrationen in `database/migrations/`, angewendet über `scripts/migrate.php`. Das Skript ist idempotent und wendet nur fehlende Migrationen an. Es arbeitet ausschließlich im Prefix aus der jeweiligen `env.local.php`; daher müssen Migrationen einmal in Staging und einmal in Produktion ausgeführt werden. Für die einmalige Übernahme vorhandener, bisher unpräfixierter Testdaten: ```bash php scripts/migrate-table-prefix.php --target-prefix=test_ --replace ``` Das Skript migriert zuerst das `test_`-Schema, ersetzt dessen Inhalt transaktional durch eine exakte Kopie der unpräfixierten Daten und vergleicht pro Tabelle die Zeilenzahl. Die unpräfixierten Quelltabellen bleiben als Rückfallmöglichkeit unverändert. Vor dem Lauf trotzdem ein vollständiges Datenbank-Backup erstellen. 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 | | Technische Löschfristen | `/httpdocs/scripts/purge-expired-operational-data.php` | stündlich | | Beendete Verträge löschen | `/httpdocs/scripts/purge-ended-contracts.php` | täglich | | 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 die Aufgaben mit Lösch- oder Buchungswirkung zuerst manuell mit `--dry-run` auslösen. Dies unterstützen `send-payment-reminders.php`, `fetch-paypal-payments.php`, `purge-expired-operational-data.php` und `purge-ended-contracts.php`. ## 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. Der Link „Zahlungsmethode verwalten“ startet technisch ausschließlich einen Stripe-Flow zur Aktualisierung der Zahlungsart. Im Stripe-Dashboard dürfen der öffentliche No-Code-Portal-Login und Tarifwechsel außerhalb der Anwendung nicht zusätzlich freigeschaltet werden; Bestellungen und Kündigungen sollen nur über die dokumentierten Abläufe der Kaffeeliste erfolgen. ## 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. ### Erreichbarkeit prüfen `scripts/check-imap-support.php` beantwortet in einem Lauf, ob dieser Host das Postfach erreichen kann: PHP-Version, ob die `imap`-Erweiterung geladen ist, ob die `PAYPAL_*`-Einstellungen gesetzt sind, und ob sich das Postfach **auch ohne die Erweiterung** über eine reine TLS-Verbindung öffnen lässt. Der Test ist nur lesend (`EXAMINE`, keine Flags), das Passwort wird nie ausgegeben. Fehlt die Erweiterung: In Plesk unter *PHP-Einstellungen* prüfen, ob `imap` angeboten wird — nachinstallieren kann sie nur der Anbieter (`plesk-php8x-imap`). Seit **PHP 8.4** ist `imap` kein Bestandteil von PHP mehr (nach PECL ausgelagert); auf 8.4/8.5 steht sie deshalb oft gar nicht mehr zur Wahl, dann hilft ein PHP-Handler auf 8.3 — oder ein Abruf über die TLS-Verbindung, die das Diagnoseskript bereits nachweist. ### 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 [--dry-run] ``` Die Verarbeitungskette selbst (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`: Zuerst aus dem Projektverzeichnis ausführen: ``` php scripts/check-production-readiness.php ``` Der Check blockiert unter anderem bei fehlender B2C-Telefonnummer, unsicherer Basis-URL, fehlenden Zahlungs-/Abrechnungsschlüsseln, zu offenen Rechten der Secret-Dateien oder einer nicht angewendeten Legal-Migration. - [ ] 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 - [ ] `LEGAL_PHONE`, `LEGAL_EMAIL` und `LEGAL_TICKET_URL` zeigen auf tatsächlich betreute Kontaktwege; Tickets werden werktags regelmäßig bearbeitet - [ ] Impressum, AGB, Datenschutz, AVV und Widerrufsbelehrung inhaltlich anwaltlich für B2B und B2C freigegeben - [ ] Kündigungs- und Widerrufsformular erzeugen beim Kunden und bei `LEGAL_EMAIL` eine E-Mail; die Textbestätigung lässt sich speichern - [ ] Offene Rechtserklärungen werden täglich im Back-Office geprüft; Rückzahlungen nach Widerruf werden manuell über Stripe fristgerecht bearbeitet (die Anwendung beendet das Abo, erstattet aber nicht automatisch) - [ ] Stripe Checkout läuft auf Deutsch, zeigt Endpreis und Monatslaufzeit; Test von Neubuchung, Tarifwechsel und Kündigung zum Periodenende - [ ] Webserver-Access-Logs werden spätestens nach sieben Tagen gelöscht oder anonymisiert; TLS/HSTS ist auf allen öffentlichen Domains aktiv - [ ] Täglicher Task `php scripts/purge-ended-contracts.php` ist eingerichtet; ein Vorlauf mit `--dry-run` wurde kontrolliert - [ ] Stündlicher Task `php scripts/purge-expired-operational-data.php` ist eingerichtet; ein Vorlauf mit `--dry-run` wurde kontrolliert - [ ] Frisches `prod_`-Schema ohne Testdaten; Staging verwendet ausschließlich `test_` ## 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.