Files
2026-09-24 17:57:54 +02:00

176 lines
9.5 KiB
Markdown

# Deployment auf cPanel („Setup Python App“ / Phusion Passenger)
Diese Anleitung beschreibt die Installation auf einem Shared-Hosting mit cPanel und
CloudLinux-„Setup Python App“. Die App braucht keine Hintergrundprozesse, keinen
Redis, keine WebSockets und keine kompilierten Pakete: Flask, python-dotenv und
SQLite reichen.
## Überblick
| cPanel-Feld | Wert |
|---|---|
| Python version | **3.10** oder neuer (neueste verfügbare wählen) |
| Application root | Ordner im Home-Verzeichnis, z. B. `glory_defense` |
| Application URL | Domain oder Unterpfad, z. B. `example.ch/` oder `example.ch/glory` |
| Application startup file | `passenger_wsgi.py` |
| Application Entry point | `application` |
## 1. Upload-Paket bauen
Auf dem eigenen Rechner im Projektordner (Windows, Linux oder macOS):
```bash
python scripts/build_deploy_zip.py
```
Das erzeugt `dist/glory_defense.zip` mit genau den Dateien, die der Server braucht:
`passenger_wsgi.py`, `requirements.txt`, `.env.example` und den Ordner `app/`
(inkl. `app/static/js/lib/phaser.min.js` und `app/static/assets/`). **Nicht**
enthalten sind `.env` (Geheimnisse), `instance/` (Datenbank), Tests, `.venv`,
`node_modules` und Caches. Node.js wird auf dem Server nicht benötigt.
> Bitte nicht mit „Komprimierter Ordner“ bzw. `Compress-Archive` aus Windows
> PowerShell 5.1 packen: Das schreibt Pfade mit Backslash (`app\api.py`) ins ZIP,
> und manche Entpacker auf Linux-Servern machen daraus flache Dateinamen. Dann
> startet die App mit `No module named 'app'` nicht. Das Skript verwendet immer `/`.
Alternativ kann das Repository über cPanel → **Git™ Version Control** direkt in den
Application-Root geklont werden.
## 2. Python-App anlegen
1. cPanel → **Software** → **Setup Python App** → **Create Application**.
2. Felder wie in der Tabelle oben ausfüllen.
3. **Create** klicken. cPanel legt den Ordner, eine virtuelle Umgebung
(`~/virtualenv/glory_defense/<version>/`) und eine Beispiel-`passenger_wsgi.py` an.
4. Oben auf der Seite erscheint der Befehl zum Aktivieren der venv, etwa:
```bash
source /home/BENUTZER/virtualenv/glory_defense/3.11/bin/activate && cd /home/BENUTZER/glory_defense
```
Diesen Befehl für Schritt 5 kopieren.
## 3. Dateien hochladen
1. cPanel → **File Manager** → in den Application-Root wechseln (z. B. `~/glory_defense`).
2. Die von cPanel erzeugte `passenger_wsgi.py` löschen oder überschreiben lassen.
3. `glory_defense.zip` hochladen und dort **Extract** wählen.
4. Prüfen: `passenger_wsgi.py` und der Ordner `app/` liegen **direkt** im Application-Root.
## 4. Umgebungsvariablen setzen
In „Setup Python App“ bei der App unter **Environment variables** eintragen und
danach **Save** klicken:
| Name | Wert |
|---|---|
| `SECRET_KEY` | langer Zufallswert, z. B. Ausgabe von `python -c "import secrets; print(secrets.token_hex(32))"` |
| `GLORY_DB_PATH` | optional, Standard `instance/glory_defense.sqlite3` (relativ zum Application-Root) |
| `GLORY_RATE_LIMIT_MAX` | optional, Standard `5` |
| `GLORY_RATE_LIMIT_WINDOW` | optional, Standard `600` (Sekunden) |
| `GLORY_TRUST_PROXY` | optional, Standard `0`. Nur `1`, wenn ein vertrauenswürdiger Proxy (z. B. CDN) davorsteht, siehe „Typische Fehler“ |
| `GLORY_NAME_BLOCKLIST` | optional, Standard `instance/name_blocklist.txt`, siehe „Moderation der Ehrentafel“ |
Alternativ eine Datei `.env` im Application-Root anlegen (Vorlage `.env.example`).
In cPanel gesetzte Variablen haben Vorrang vor der `.env`.
> Ohne `SECRET_KEY` erzeugt die App beim ersten Spielstart `instance/secret_key`
> selbst. Das funktioniert, ein eigener Wert in cPanel ist aber sauberer und
> übersteht Neuinstallationen.
## 5. Abhängigkeiten installieren
**Variante A, über die Oberfläche:** In „Setup Python App“ bei der App unter
*Configuration files* `requirements.txt` eintragen, **Add** und dann
**Run Pip Install** klicken.
**Variante B, per Terminal** (cPanel → **Terminal** oder SSH):
```bash
source /home/BENUTZER/virtualenv/glory_defense/3.11/bin/activate && cd /home/BENUTZER/glory_defense
pip install -r requirements.txt
```
Die Datenbank legt die App beim ersten Aufruf selbst an, ein eigener Schritt ist
nicht nötig.
## 6. Neustarten und prüfen
- In „Setup Python App“ auf **Restart** klicken, oder per Terminal:
```bash
mkdir -p tmp && touch tmp/restart.txt
```
- Im Browser aufrufen:
- `https://example.ch/healthz` muss `{"status":"ok"}` liefern
- `https://example.ch/` zeigt den Disclaimer und danach das Hauptmenü
- `https://example.ch/api/highscores` liefert `{"highscores":[]}`
Läuft die App unter einem Unterpfad (z. B. `/glory`), funktionieren alle Links
automatisch: Das Template erzeugt URLs über `url_for`, und Passenger übergibt den
Pfad als `SCRIPT_NAME`.
## 7. Updates einspielen
1. Lokal `python scripts/build_deploy_zip.py` ausführen, das neue ZIP hochladen und
entpacken (vorhandene Dateien überschreiben). `instance/` dabei **nicht** löschen,
denn dort liegen Highscores, Sperrliste und ggf. der automatisch erzeugte Schlüssel.
2. Falls sich `requirements.txt` geändert hat: **Run Pip Install** bzw. `pip install -r requirements.txt`.
3. **Restart** (oder `touch tmp/restart.txt`).
4. Im Browser mit Strg+F5 neu laden, damit keine alten JavaScript-Dateien aus dem Cache kommen.
## 8. Datensicherung
Zu sichern ist nur der Ordner `instance/`:
- `instance/glory_defense.sqlite3`: Highscores
- `instance/name_blocklist.txt`: Sperrliste, falls angelegt
- `instance/secret_key`: nur vorhanden, wenn kein `SECRET_KEY` gesetzt ist
## 9. Moderation der Ehrentafel
Spielernamen erscheinen öffentlich. Unpassende Einträge (Beleidigungen, Namen realer
Personen) lassen sich im Terminal löschen. Zuerst die venv aktivieren (Befehl aus
Schritt 2.4), dann:
```bash
flask --app passenger_wsgi scores list # Einträge mit ID anzeigen
flask --app passenger_wsgi scores delete 17 23 # Einträge per ID löschen
flask --app passenger_wsgi scores delete-name "Name" # alle Einträge mit diesem Namen löschen
```
Die Befehle brauchen keinen `SECRET_KEY`. Das ist wichtig, denn in cPanel gesetzte
Umgebungsvariablen stehen im Terminal nicht zur Verfügung. Eine `.env` im
Application-Root wird dagegen auch hier gelesen.
**Sperrliste (optional):** Die Datei `instance/name_blocklist.txt` anlegen, ein Begriff
pro Zeile, `#` leitet Kommentare ein. Namen, die einen dieser Begriffe enthalten,
lehnt die API ab. Groß-/Kleinschreibung, Leerzeichen und Satzzeichen spielen dabei
keine Rolle, „Max Muster“ sperrt also auch „max.muster“. Die Datei wird bei jeder
Einreichung neu gelesen, ein Neustart ist nicht nötig.
## Typische Fehler
| Symptom | Ursache und Lösung |
|---|---|
| „Incomplete response received from application“ oder Fehler 500 | Details stehen in `stderr.log` im Application-Root bzw. in cPanel → *Errors*. Meist fehlen die Abhängigkeiten (Schritt 4) oder die Startdatei ist falsch eingetragen. |
| `ModuleNotFoundError: No module named 'app'` | `passenger_wsgi.py` liegt nicht im Application-Root, der Ordner `app/` fehlt bzw. liegt eine Ebene zu tief (ZIP in einen Unterordner entpackt), oder das ZIP wurde mit Backslash-Pfaden erstellt (Dateien heißen dann `app\api.py`). Paket mit `python scripts/build_deploy_zip.py` bauen. |
| `ModuleNotFoundError: No module named 'flask'` | Pip-Install lief in der falschen Umgebung. Befehl aus Schritt 2.4 verwenden (venv aktivieren) und erneut installieren (Schritt 5). |
| `SyntaxError` oder Flask-Importfehler beim Start | Python-Version zu alt. In „Setup Python App“ **3.10 oder neuer** wählen (die App kann dafür neu angelegt werden, Dateien bleiben erhalten). |
| `sqlite3.OperationalError: unable to open database file` | Der Ordner der Datenbank ist nicht beschreibbar. `instance/` muss dem cPanel-Benutzer gehören (Rechte 755). `GLORY_DB_PATH` nicht auf einen fremden Ordner setzen. |
| `database is locked` bei viel Last | SQLite wartet bis zu 10 s auf Sperren. Bei sehr vielen gleichzeitigen Einreichungen hilft ein kleineres Rate-Limit. Für ein Spiel mit Highscore-Liste reicht SQLite in der Regel. |
| Seite lädt, Spiel bleibt schwarz, Konsole zeigt 404 für `phaser.min.js` | `app/static/js/lib/phaser.min.js` wurde nicht mit hochgeladen (manche FTP-Clients überspringen große Dateien). Das Upload-ZIP aus Schritt 1 enthält sie. |
| Änderungen nicht sichtbar | App neu starten (Schritt 6) und Browser-Cache leeren (Strg+F5). |
| Alle Spieler teilen sich ein Rate-Limit | Ein vorgeschalteter Proxy (z. B. CDN) liefert allen dieselbe IP. Wenn der Proxy `X-Forwarded-For` zuverlässig setzt, `GLORY_TRUST_PROXY=1` setzen. Sonst beim Hoster `mod_remoteip` erfragen. |
| `/healthz` ok, aber Highscores werden mit „Run-Token ungültig“ abgelehnt | `SECRET_KEY` wurde zwischen Spielstart und Einreichen geändert (z. B. durch Neustart mit neuem Wert). Einfach eine neue Runde spielen. |
## Sicherheitshinweise
- Die App setzt eine strikte Content-Security-Policy. Das Spiel lädt keine Skripte
von fremden Servern.
- IP-Adressen werden nicht gespeichert, nur ein HMAC-Hash (mit `SECRET_KEY`) für das
Rate-Limit. Einträge werden nach Ablauf des Zeitfensters automatisch gelöscht.
- Spielernamen werden bereinigt (max. 16 Zeichen, keine Steuer- oder Sonderzeichen)
und im Frontend immer als Text ausgegeben, nie als HTML. Unpassende Einträge lassen
sich löschen oder per Sperrliste verhindern (Abschnitt 9).
- Mit `GLORY_TRUST_PROXY=1` übernimmt die App vom Proxy nur Client-IP und Protokoll,
nicht Host oder Pfadpräfix. So lassen sich keine fremden URLs in die Seite schleusen.