# 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//`) 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.