Deployment-Vorbereitung: Ausschlussliste, .htaccess-Haertung, Doku
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>
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user