Vor dem ersten FTP-Deploy nach /testumgebung.kaffeeliste.de/httpdocs/ fehlte jede Absicherung des Upload-Umfangs: sync_config.jsonc hatte eine leere excludePath-Liste, es waeren also .git (komplett herunterladbar), .env.local (Dev-DB-Zugangsdaten) und sync_config.jsonc selbst (enthaelt das FTP-Passwort im Klartext) mit ausgeliefert worden. Von diesen dreien war keines von den bestehenden .htaccess-Regeln erfasst. - sync_config.jsonc: excludePath gefuellt; scripts/ und database/ bleiben bewusst im Deploy, weil die Plesk-Scheduled-Tasks sie vom Webspace aus ausfuehren. - .htaccess: sync_config.jsonc und Dotfile-Ordner gesperrt, Vendor- Verzeichnisse (TCPDF/PHPMailer/DataTables) fuer direkte URL-Aufrufe gesperrt, HTTPS-Redirect ergaenzt (ohne ihn bekam ein http-Besucher eine Session ohne secure-Flag). - PHPMailer/ entfernt: enthielt nur noch LICENSE, composer.json und ein per URL erreichbares get_oauth_token.php, kein Quellcode. Der Versand laeuft seit M6 ueber saas_send_mail(). - docs/deployment.md: Umgebungen, Deploy-Ablauf, Migrationen und Cron-Jobs ueber Plesk Scheduled Tasks (kein SSH verfuegbar), Mail-/DNS-, Stripe- und PayPal-Voraussetzungen, Go-Live-Checkliste. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
280 lines
11 KiB
Markdown
280 lines
11 KiB
Markdown
# 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://<host>/sync_config.jsonc
|
||
https://<host>/.env.local
|
||
https://<host>/.git/config
|
||
https://<host>/scripts/migrate.php
|
||
https://<host>/docs/deployment.md
|
||
https://<host>/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://<host>/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+<token>@…` ankommen. IMAP-Zugangsdaten in
|
||
`env.local.php` eintragen und die `imap`-Extension auf dem Host prüfen.
|
||
|
||
## 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.
|