B1 auf dem Webspace durchgeführt: PHP-CLI 8.3.29 unter /usr/bin/php, Composer 2.9.8 global, Git 2.43. Der Ordnername tsv08kulmbach.de ist von der alten CMS-Seite belegt (die der Rückweg beim Umschalten ist), der neue heißt deshalb tsv08kulmbach-website. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
12 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 <dein-kas-user>@<dein-server>.kasserver.com
pwd # z. B. /www/htdocs/w01ca75d → das ist HOME
which php; php -v # PHP muss >= 8.1 sein
git --version
Ergebnis vom 31.07.2026 auf diesem Webspace:
| HOME | /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 |
All-Inkl legt pro Domain einen Ordner im Home an, das Home 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 ~
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
scp config/config.production.php \
<kas-user>@<server>.kasserver.com:~/tsv08kulmbach-website/config/config.php
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 ~/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
- Domains → Subdomain
neu.tsv08kulmbach.deanlegen. - Als Verzeichnis
/tsv08kulmbach-website/publiceintragen, nicht das Repo-Root. Damit liegenapp/,config/,data/,storage/undvendor/außerhalb des Docroots und sind grundsätzlich nicht per URL erreichbar. (Die Root-.htaccessist nur ein Notnagel für Hoster, die das nicht können.) - PHP-Version für die Subdomain auf 8.1 oder neuer stellen.
- SSL aktivieren (Let's Encrypt). Ohne Zertifikat läuft die Seite in eine
Endlosweiterleitung, denn
public/.htaccesserzwingt HTTPS. - 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 ~/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,/veranstaltungenplus eine Detailseite/stadionzeitungund die Ausgabe durchswipen — alle Wappen und Instagram-Bilder da? (Sie liegen im Repo, sollten also stimmen.)/stadionzeitung/aktuelllandet auf der neuesten Ausgabe- Die Druckfassung einer Ausgabe (
?druck=1) /impressum: Webadresse zeigtwww.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).
/dateienmit 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.phpzeigt 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
Domains → tsv08kulmbach.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.delädthttp://tsv08kulmbach.de(ohne www, ohne s) landet per 301 aufhttps://www.tsv08kulmbach.de/sitemap.xmlund/robots.txterreichbarcurl -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 ~/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 ~/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.xmleinreichen. - 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.phpwarnt beim nächsten Lauf.