Files
tsv08kulmbach-website/docs/deploy.md
fs 3cf8c2688b Deploy-Anleitung: Platzhalter ohne spitze Klammern
<host> liest die zsh als Eingabe-Umleitung, der scp brach mit "no such file or
directory: host" ab. Platzhalter heißen jetzt SERVER.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 16:28:58 +02:00

13 KiB

Deploy auf All-Inkl (Shared Hosting, SSH + Cronjobs)

Schritt für Schritt vom leeren Webspace zur Live-Seite. Die Reihenfolge ist Absicht: die alte CMS-Seite bleibt online, bis die neue auf einer geschützten Test-Subdomain nachweislich läuft. Umgeschaltet wird erst am Ende, und das Umschalten ist in einer Minute rückgängig.

Der Kern des Plans: die Test-Subdomain läuft mit derselben config.php, die später live geht. Es gibt keinen Config-Wechsel beim Umschalten, also auch keinen Fehler, der sich dabei einschleichen kann. Möglich ist das, weil die Host-Weiterleitung in public/.htaccess nur beim exakten Host tsv08kulmbach.de greift; eine Subdomain läuft unbehelligt durch. Einzige Abweichung: Canonical, OG und Sitemap nennen auf der Test-Subdomain schon die echte Domain. Genau richtig für den Umschaltmoment, und dank Verzeichnisschutz sieht es ohnehin keine Suchmaschine.

Legende: [lokal] auf deinem Mac, [SSH] in der Server-Shell, [KAS] im All-Inkl-Kundenmenü.


Teil A — Vorbereitung lokal

A1 [lokal] Die zwei offenen Werte in config/config.production.php setzen

php bin/preflight.php bricht sonst auf dem Server ab. Beides betrifft nur die Dateiablage /dateien, nicht die Website selbst.

Passwörter für die zwei Zugänge. Als Hash, nicht als Klartext:

php -r 'echo password_hash("DEIN-PASSWORT", PASSWORD_DEFAULT), "\n";'

Ausgabe (beginnt mit $2y$…) in dateien.users.verein.password_hash bzw. dateien.users.admin.password_hash eintragen. Zwei verschiedene Passwörter: verein gibt der Verein weiter, admin behältst du.

Lizenzschlüssel der Files Gallery in dateien.license_key. Fehlt er, läuft die Ablage trotzdem, zeigt aber den Hinweis der unlizenzierten Version (nur eine Warnung in preflight, kein Blocker).

Bereits erledigt: cron.key ist auf '' gesetzt und der Endpoint public/cron.php damit zu. Der Takt kommt ab jetzt aus echten Cronjobs (Teil D), der Endpoint war nur der Ersatz für den fehlenden Cron.

A2 [lokal] Alles gepusht?

git status --porcelain     # muss leer sein
git push origin main

Teil B — Server einrichten

B1 [SSH] Umgebung feststellen und notieren

ssh ssh-w01ca75d@SERVER      # SERVER = Hostname oder IP aus dem KAS
pwd                # das Verzeichnis, in dem SSH startet
which php; php -v  # PHP muss >= 8.1 sein
git --version

Ergebnis vom 31.07.2026 auf diesem Webspace:

Webspace-Wurzel /www/htdocs/w01ca75d
SITE /www/htdocs/w01ca75d/tsv08kulmbach-website
PHP (CLI, für Cron) /usr/bin/php — 8.3.29
Composer 2.9.8, global vorhanden
Alte CMS-Seite (Rückweg) /www/htdocs/w01ca75d/tsv08kulmbach.de

Falle: ~ ist hier /, nicht der Webspace. Das SSH-Konto hat die Systemwurzel als Home-Verzeichnis, cd ~ führt also aus dem Webspace heraus und ~/…-Pfade existieren nicht. Alle Pfade in dieser Anleitung sind deshalb absolut — auch das scp-Ziel und die Cron-Zeilen.

All-Inkl legt pro Domain einen Ordner unter /www/htdocs/<user>/ an, dieses Verzeichnis selbst ist also kein Docroot. Der Ordnername tsv08kulmbach.de ist von der alten Seite belegt, deshalb heißt der neue tsv08kulmbach-website. Die anderen TSV-Ordner (2024., hall-of-fame., kabinendienst., kreisliga-reise.) sind eigene Subdomains und vom Umschalten nicht betroffen.

Achtung: das ist die CLI-Version. Welches PHP der Webserver nimmt, stellt KAS pro Domain getrennt ein (Schritt C1).

B2 [SSH] Repo klonen

cd /www/htdocs/w01ca75d
git clone https://onion.breadcrumb-online.de/fs/tsv08kulmbach-website.git tsv08kulmbach-website
cd tsv08kulmbach-website && pwd     # diesen Pfad notieren: das ist SITE

Der Klon lädt gut 460 MB (Website plus das Vereinsmaterial der Dateiablage), das dauert ein paar Minuten. Die alte CMS-Seite liegt in einem anderen Verzeichnis und wird nicht angefasst.

B3 [SSH] config.php platzieren

Die Datei ist gitignored, also nicht im Klon. Vom Mac hochladen:

# [lokal], in einem zweiten Terminal.
# SERVER = derselbe Host wie beim ssh-Login (hier die Server-IP des Webspace).
scp config/config.production.php \
    ssh-w01ca75d@SERVER:/www/htdocs/w01ca75d/tsv08kulmbach-website/config/config.php

Platzhalter bewusst ohne spitze Klammern: <host> liest die zsh als Eingabe-Umleitung und bricht mit „no such file or directory" ab.

Wichtig: Ziel heißt config.php, nicht config.production.php. Danach [SSH]:

chmod 600 config/config.php     # enthält SMTP-Passwort und app_secret

B4 [SSH] Composer-Abhängigkeiten

cd /www/htdocs/w01ca75d/tsv08kulmbach-website
composer install --no-dev --optimize-autoloader

Kennt die Shell composer nicht, hol ihn dir lokal ins Verzeichnis:

curl -sS https://getcomposer.org/installer | php
php composer.phar install --no-dev --optimize-autoloader
rm composer.phar

Es wird nur PHPMailer installiert.

B5 [SSH] Schreibrechte prüfen

PHP läuft unter deinem Benutzer, meist passt es also schon. preflight sagt es dir in Teil C4 verbindlich. Falls dort etwas fehlt:

chmod -R u+rwX storage

Schreiben muss die Seite in storage/logs, storage/cache, storage/ratelimit, storage/dateien/uploads und storage/dateien/system.


Teil C — Auf der Test-Subdomain prüfen

C1 [KAS] Subdomain anlegen und schützen

  1. Domains → Subdomain neu.tsv08kulmbach.de anlegen.
  2. Als Verzeichnis /tsv08kulmbach-website/public eintragen, nicht das Repo-Root. Damit liegen app/, config/, data/, storage/ und vendor/ außerhalb des Docroots und sind grundsätzlich nicht per URL erreichbar. (Die Root-.htaccess ist nur ein Notnagel für Hoster, die das nicht können.)
  3. PHP-Version für die Subdomain auf 8.1 oder neuer stellen.
  4. SSL aktivieren (Let's Encrypt). Ohne Zertifikat läuft die Seite in eine Endlosweiterleitung, denn public/.htaccess erzwingt HTTPS.
  5. Verzeichnisschutz für die Subdomain einschalten (Basic Auth). Das ist der Grund, warum hier nichts indexiert werden kann. Nicht weglassen.

C2 Erreichbarkeit

https://neu.tsv08kulmbach.de aufrufen. Nach der Passwortabfrage muss die Startseite kommen. Weißes Blatt oder 500er? Dann [SSH] tail -30 storage/logs/php-errors.log.

C3 [SSH] Datenstände erzeugen

data/matchcenter.json und data/instagram.json sind bewusst nicht im Repo, die holen die Syncs. Vor dem ersten Lauf fehlen Tabellen, Spiele und der Instagram-Block (die Seite bricht deswegen nicht, sie zeigt Ersatzinhalte).

cd /www/htdocs/w01ca75d/tsv08kulmbach-website
php bin/matchcenter-sync.php     # Spiele, Ergebnisse, Tabellen, Wappen
php bin/instagram-sync.php       # Posts und Bilder

Der Instagram-Lauf ist der Wackelkandidat des ganzen Deploys. Instagram sperrt Rechenzentrums-IPs; bisher lief der Scraper nur über einen privaten Anschluss. Schlägt er hier fehl, ist das kein Fehler im Deploy: dann bleibt der Weg, ihn von zu Hause laufen zu lassen und data/instagram.json samt Bildern hochzuladen, oder der Block wird redaktionell gepflegt. Die Website funktioniert in jedem Fall.

C4 [SSH] preflight bis null Fehler

php bin/preflight.php; echo "Exit: $?"

Ziel ist Exit 0. Warnungen darfst du bewerten, Fehler nicht. Erwartbare Warnungen hier: der Hinweis, dass cron.key leer ist (richtig so, siehe A1) und die Uploadgrenze, die erst ein echter Upload beweist (C6).

C5 Durchklicken

Die Seiten, die eigene Datenquellen haben:

  • Startseite: Banner-Slider, Termin-Karte im Hero, Instagram-Block
  • /matchcenter: Tabellen und nächste Spiele mit Wappen
  • /termine, /news, /veranstaltungen plus eine Detailseite
  • /stadionzeitung und die Ausgabe durchswipen — alle Wappen und Instagram-Bilder da? (Sie liegen im Repo, sollten also stimmen.)
  • /stadionzeitung/aktuell landet auf der neuesten Ausgabe
  • Die Druckfassung einer Ausgabe (?druck=1)
  • /impressum: Webadresse zeigt www.tsv08kulmbach.de, nicht die Subdomain

C6 Dateiablage und Mailversand

php bin/dateien-init.php          # Jahr/Abteilung-Ordner, MUSS auf dem Server laufen
php bin/smtp-test.php             # nur Verbindung und Login, kein Versand

dateien-init.php gehört auf den Server, weil Umlaute per FTP zu Doppelordnern führen (macOS NFD gegen Linux NFC).

  • /dateien mit beiden Zugängen anmelden
  • Eine große Datei hochladen (>10 MB) — beweist die 250-MB-Grenze
  • Ein Formular abschicken (z. B. /kontakt) und die Mail im Postfach prüfen
  • php bin/logs.php zeigt keine Fehler

Teil D — Cronjobs

Erst jetzt, damit ab dem Umschalten schon frische Daten liegen. SITE und der PHP-Pfad kommen aus B1/B2.

[KAS] → Cronjobs → eigener Cronjob (Shell), vier Einträge:

# Instagram 2x/Tag
17 6,18 * * *        cd SITE && PHP bin/instagram-sync.php >> storage/logs/instagram.log 2>&1

# Matchcenter Grundtakt 2x/Tag
37 7,19 * * *        cd SITE && PHP bin/matchcenter-sync.php >> storage/logs/matchcenter.log 2>&1

# Matchcenter am Spieltag halbstuendlich (Sa+So 13-21 Uhr)
7,37 13-21 * * 6,0   cd SITE && PHP bin/matchcenter-sync.php >> storage/logs/matchcenter.log 2>&1

# Logpflege sonntags nachts
23 3 * * 0           cd SITE && PHP bin/log-rotate.php > /dev/null 2>&1

SITE und PHP durch die echten absoluten Pfade ersetzen, auf diesem Webspace also cd /www/htdocs/w01ca75d/tsv08kulmbach-website && /usr/bin/php bin/…. Die Minuten sind versetzt, damit sich zwei Läufe nicht überlappen. Alle Skripte sind CLI-only, sperren sich per Lock gegen Parallelläufe und lassen bei Fehlern den letzten guten Cache stehen — ein fehlgeschlagener Lauf schadet nie.

Am Tag nach dem ersten Cron-Lauf einmal php bin/logs.php ansehen. Läuft nichts, ist fast immer der PHP-Pfad falsch oder relativ.


Teil E — Umschalten auf die Hauptdomain

Ab hier ist die Seite öffentlich. Alles davor muss sitzen.

E1 Sicherheitsnetz

Notiere, auf welches Verzeichnis www.tsv08kulmbach.de jetzt zeigt (die alte CMS-Seite). Das ist dein Rückweg. Verzeichnis und Datenbank der alten Seite bleiben unangetastet, nicht löschen.

E2 [KAS] Domain umstellen

Domainstsv08kulmbach.de und www.tsv08kulmbach.de auf /tsv08kulmbach-website/public zeigen lassen. PHP-Version prüfen, SSL muss für beide aktiv sein.

E3 Sofort danach prüfen

  • https://www.tsv08kulmbach.de lädt
  • http://tsv08kulmbach.de (ohne www, ohne s) landet per 301 auf https://www.tsv08kulmbach.de
  • /sitemap.xml und /robots.txt erreichbar
  • curl -sI https://www.tsv08kulmbach.de | head — Statuszeile 200, kein Redirect-Loop
  • Ein Formular live abschicken
  • Einen gedruckten QR-Code mit dem Handy scannen. Der Code der Stadionzeitung zeigt auf www.tsv08kulmbach.de/stadionzeitung/aktuell. Scannen ist die einzige echte Prüfung: die QR-Bibliothek arbeitet nicht reproduzierbar, ein Bildvergleich beweist nichts.

E4 Verzeichnisschutz der Test-Subdomain lassen

Nicht entfernen. Sonst steht die Seite zweimal im Netz und Google sieht doppelten Inhalt. Wenn du die Subdomain nicht mehr brauchst, in KAS löschen.


Teil F — Danach

F1 Der Update-Ablauf ab jetzt

Die Reihenfolge ist nicht verhandelbar, weil storage/dateien/uploads/ im Repo liegt und andere Menschen dort hochladen:

# 1. [SSH] auf dem Server: neue Uploads sichern
cd /www/htdocs/w01ca75d/tsv08kulmbach-website
git add -A storage/dateien/uploads && git commit -m "Uploads vom Server" && git push

# 2. [lokal] holen, dann entwickeln
git pull

# 3. [lokal] fertig, gepusht → [SSH] ausrollen
cd /www/htdocs/w01ca75d/tsv08kulmbach-website && git pull && php bin/preflight.php

Nie in der anderen Reihenfolge. Details und Begründung in CLAUDE.md, Abschnitt Dateiablage.

Ändert sich composer.json, gehört ein composer install --no-dev --optimize-autoloader dazu. Neue Formularfelder heißen: Datenschutzerklärung mitziehen.

F2 Rückweg, falls etwas Grundlegendes bricht

[KAS] Domain zurück auf das alte Verzeichnis aus E1. Nach ein paar Minuten ist die alte Seite wieder da. Kein Datenverlust, weil die neue Seite in einem eigenen Verzeichnis liegt und die alte Datenbank nicht anfasst.

F3 Offen, nach dem Livegang

  • 301-Weiterleitungen der alten URLs. Die neue Seite hat andere Slugs; alte Google-Treffer und fremde Links laufen ins Leere. Die alten Adressen stehen im DB-Dump unter .context_db. Aufwand lohnt für die paar wichtigen Seiten, nicht für alle.
  • Google Search Console: Property anlegen, sitemap.xml einreichen.
  • Instagram-Sync beobachten, siehe C3.
  • Nach dem Sportfest (ab 07.09.2026) fallen zwei Anzeigen per bis-Datum aus dem Bestand, die Stadionzeitung hat dann 38 Seiten statt 40 und ist nicht mehr durch 4 teilbar. bin/stadionzeitung-add.php warnt beim nächsten Lauf.