Von einer Shared-Hosting-PHP-App zu moderner Infrastruktur migrieren

Sandor Farkas
Gründer & Lead Developer
Experte für Softwareentwicklung und Legacy-Code-Optimierung
LinkedInErstaunlich viele funktionierende europäische Unternehmen betreiben ihre Kernsoftware noch immer auf Shared-Hosting-Konten, die Anfang der 2010er-Jahre eingerichtet wurden. Die Anwendung ist PHP 7.2 (manchmal 5.6), läuft unter suPHP auf einem Plesk-Control-Panel, wird durch Hineinziehen von Dateien in FileZilla deployt, und die einzige "Backup-Strategie" ist das, was der Hosting-Anbieter zufällig um drei Uhr morgens tut. Es gibt keine Git-Historie. Es gibt drei Ordner namens /admin_old/, /admin_old2/ und /admin_FINAL/. Die einzige Person, die den Deployment-Workflow jemals verstanden hat, nahm 2019 einen Job in München an.
Das ist kein seltener Sonderfall. Ein erheblicher Teil des deutschen Mittelstands, französische B2B-Dienstleister und familiengeführte europäische E-Commerce-Betriebe laufen genau auf dieser Art von Stack. Die Shared-Hosting-PHP-App generiert echten Umsatz. Sie macht aber auch jede Änderung riskant, jedes Audit unbeantwortbar und jedes Entwicklergespräch schwierig.
Eine erfolgreiche PHP-Shared-Hosting-Migration zu moderner Infrastruktur (containerisiert, versionskontrolliert, von einer Pipeline deployt) erfordert keinen Neuschrieb. Sie erfordert eine bewusste Abfolge kleiner, umkehrbarer Schritte, die die Anwendung durchgehend am Laufen halten. Dieser Beitrag führt durch das Playbook, das wir bei Wolf-Tech anwenden, wenn wir diese Kategorie von Legacy-PHP-Deployments modernisieren.
Warum Shared Hosting zur Belastung wird
Der Grund, warum Teams bei Shared Hosting bleiben, ist einfach: Es funktioniert. Die Anwendung bedient Kunden, die monatliche Rechnung ist klein, und etwas zu ändern birgt das Risiko, die einzige Umsatzquelle des Unternehmens zu zerstören. Die Trägheit ist rational.
Was die Rechnung verändert, ist der Moment, in dem die verborgenen Kosten einen einholen. Shared-Hosting-Konten begrenzen PHP-Versionen typischerweise auf das, was der Anbieter unterstützt, was zwei oder drei Hauptversionen hinterherhinkt. Bis 2026 läuft ein Konto mit PHP 7.4 seit über zwei Jahren auf einer nicht mehr unterstützten Runtime, und die Sicherheitspatches, von denen die Anwendung abhängt, werden nicht mehr ausgeliefert. Composer-Abhängigkeiten verweigern die Installation, weil sie PHP 8.1+ benötigen. Moderne Bibliotheken sind nicht verfügbar.
Betrieblich bedeutet das Fehlen von Versionskontrolle, dass jede Änderung eine manuelle Dateibearbeitung auf einem Produktionsserver ist, und die einzige Möglichkeit, herauszufinden, was sich geändert hat, ist der Vergleich von Zeitstempeln. Rollbacks erfordern ein Backup, das es vielleicht gibt oder nicht. Es gibt keine Staging-Umgebung, weil das Erstellen einer solchen ein weiteres zu verwaltendes Hosting-Konto bedeuten würde. Mehrere Entwickler können nicht gleichzeitig an der Codebasis arbeiten, ohne sich per Chat abzustimmen, um nicht gegenseitig ihre Änderungen zu überschreiben.
Diese Einschränkungen zeigen sich nicht als ein einzelnes dramatisches Versagen. Sie zeigen sich als stetig langsamere Feature-Auslieferung, wachsende Scheu, irgendetwas anzufassen, und schließlich als Unfähigkeit, kompetente Entwickler einzustellen, weil kein erfahrener Ingenieur so arbeiten will. Bis ein Unternehmen sich zur Modernisierung entschließt, hat die Anwendung oft jahrelang zufällige Komplexität angesammelt, die die Migration schwerer macht, als sie früher gewesen wäre.
Schritt eins: Die Codebasis unter Versionskontrolle bringen, bevor irgendetwas geändert wird
Der erste Schritt jeder PHP-Shared-Hosting-Migration ist, eine Quelle der Wahrheit für den Code zu schaffen. Bevor sich irgendetwas anderes ändert (kein PHP-Versions-Upgrade, keine Infrastrukturarbeit, kein Refactoring), muss die Anwendung in Git leben, mit dem Produktionsserver als kanonischem Ausgangspunkt.
Die Mechanik ist banal, aber es lohnt sich, sie sorgfältig zu erledigen. Verwende SFTP oder rsync, um das gesamte Document-Root und alle zugehörigen Ordner (Cron-Skripte, Includes außerhalb des Web-Roots, Konfigurationsdateien) in ein lokales Arbeitsverzeichnis zu ziehen. Initialisiere ein Git-Repository, erstelle eine .gitignore, die Runtime-Ordner ausschließt (hochgeladene Nutzerdateien, Cache-Verzeichnisse, Logs), und committe alles als ersten Commit mit einer Nachricht wie "Initial import from production, 2026-04-22".
# Anwendung vom Shared Hosting ziehen
rsync -avz --exclude='cache/' --exclude='logs/' --exclude='uploads/' \
user@oldhost.example.com:/var/www/vhosts/app/httpdocs/ ./app-import/
cd app-import
git init
git add .
git commit -m "Initial import from production, 2026-04-22"
git remote add origin git@github.com:company/legacy-app.git
git push -u origin main
Der nächste Commit sollte eine README.md sein, die dokumentiert, was beim Import entdeckt wurde: die PHP-Version in Produktion, den Namen und die Version des Datenbankservers, die im Control-Panel registrierten Cron-Jobs, alle Drittanbieterdienste, die die Anwendung aufruft, sowie alle Umgebungsvariablen oder Konfigurationskonstanten. Dieses Dokument ist der Anker der Migration. Es hält die Welt fest, wie sie ist, bevor jemand versucht, sie zu ändern.
Wichtig: Refactore noch nichts. Die Versuchung, das offensichtliche Chaos aufzuräumen (die doppelten Ordner, der tote Code, die inline hinterlegten Zugangsdaten), ist groß. Widerstehe ihr. Refactoring, bevor du eine Baseline hast, macht es unmöglich zu wissen, ob ein Bug durch das Refactoring eingeführt wurde oder vorher existierte. Die Codebasis muss exakt so reproduzierbar sein, wie sie in Produktion lief, bevor irgendeine Bereinigung beginnt.
Schritt zwei: Containerisieren, ohne die Anwendung zu ändern
Der zweite Schritt ist, die Anwendung lokal und in einer kontrollierten Umgebung zum Laufen zu bringen, identisch zu der Art, wie sie auf dem Shared Host läuft. Das richtige Werkzeug dafür ist Docker, konfiguriert, um die Produktions-Runtime so genau wie möglich nachzubilden.
Der Containerisierungsschritt für PHP ist der Punkt, an dem die meisten Modernisierungsbemühungen scheitern, indem sie zu viel auf einmal versuchen. Das Ziel hier ist nicht, PHP zu aktualisieren, Webserver zu wechseln oder die Anwendung umzustrukturieren. Das Ziel ist, ein Docker-Image zu erstellen, das die bestehende Anwendung unverändert ausführt (auf derselben PHP-Version, mit denselben Erweiterungen, gegen eine Datenbank, die die Produktion spiegelt), damit das Team entwickeln und testen kann, ohne den Live-Server anzufassen.
Ein pragmatisches Dockerfile für diese Phase sieht so aus:
# Die Produktions-PHP-Version exakt nachbilden, auch wenn sie alt ist
FROM php:7.4-apache
# Dieselben Erweiterungen installieren, die der Shared Host aktiviert hatte
RUN apt-get update && apt-get install -y \
libpng-dev libjpeg-dev libfreetype6-dev libzip-dev unzip \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install gd mysqli pdo_mysql zip opcache
# Produktions-php.ini-Einstellungen nachbilden (memory_limit, Upload-Groessen, Zeitzone)
COPY docker/php.ini /usr/local/etc/php/php.ini
# Apache mod_rewrite aktivieren, falls die Legacy-App .htaccess verwendet
RUN a2enmod rewrite
# Anwendungscode kopieren
COPY . /var/www/html/
# Dateibesitz nachbilden, den der Shared Host verwendet hat
RUN chown -R www-data:www-data /var/www/html
Führe dies mit einer docker-compose.yml aus, die den Anwendungscontainer mit einem MySQL- oder MariaDB-Container in derselben Hauptversion wie die Produktion paart, befüllt mit einem bereinigten Snapshot der Produktionsdatenbank. Sobald die Anwendung in dieser Umgebung das erste Mal startet und eine echte Seite ausliefert, hat das Team eine bedeutsame Schwelle überschritten: Die Anwendung existiert nun an einem anderen Ort als dem Shared-Hosting-Konto, und Änderungen können vor dem Deployment getestet werden.
Das ist auch der Moment, die impliziten Abhängigkeiten zu entdecken, die der Shared Host stillschweigend bereitstellte. Fehlende PHP-Erweiterungen, von der Anwendung angenommene Dateiberechtigungen, Pfade, die fest auf die Verzeichnisstruktur des alten Hosting-Anbieters codiert sind, Mail-Konfiguration, die funktionierte, weil der Shared Host ein lokales SMTP-Relay hatte: All das taucht während der Containerisierung auf. Dokumentiere jeden Punkt und behebe ihn entweder in der Container-Konfiguration oder notiere ihn als Deployment-Anforderung für die neue Infrastruktur.
Schritt drei: Parallele moderne Infrastruktur aufsetzen
Sobald die Anwendung containerisiert ist, besteht der nächste Schritt darin, die Zielinfrastruktur bereitzustellen. Das Ziel hängt von der Größe der Anwendung und der betrieblichen Reife des Teams ab, doch für die meisten europäischen mittelgroßen PHP-Anwendungen ist der richtige Ausgangspunkt ein einzelner virtueller Server bei einem Anbieter mit europäischen Rechenzentren (Hetzner, OVH, Scaleway oder AWS Frankfurt), der Docker betreibt, mit Nginx und Let's-Encrypt-TLS-Zertifikaten davor und einem Managed-Database-Dienst für MySQL oder PostgreSQL.
Die Architektur vermeidet bewusst Over-Engineering. Kubernetes, Multi-Region-Deployments und Serverless-Plattformen sind verlockend, aber in dieser Phase unnötig. Der Erfolg der Migration hängt davon ab, die Zahl der gleichzeitig veränderten Variablen zu minimieren. Ein einzelner VPS, der Docker Compose betreibt, ist enorm viel leistungsfähiger als das Shared-Hosting-Konto, das er ersetzt, und er bietet ein Fundament, das sich später zu ausgefeilteren Architekturen entwickeln kann.
Die Managed Database ist der eine Punkt, an dem es sich lohnt, Geld auszugeben, statt selbst zu hosten. Backup, Point-in-Time-Recovery, Versions-Upgrades und Replikation sind betriebliche Anliegen, die Managed Services gut lösen und die niemand in einem kleinen Team um drei Uhr morgens manuell erledigen will. RDS, Hetzners Managed PostgreSQL oder DigitalOceans Managed MySQL funktionieren alle gut für dieses Profil.
Die Migration der Datenbank selbst verdient explizite Planung. Für eine kleine Datenbank (unter 10 GB) genügt ein mysqldump-Export vom Shared Host gefolgt von einem Import in die Managed-Instanz. Für größere Datenbanken oder Anwendungen mit geringer Toleranz für Schreib-Ausfallzeiten lässt MySQL-Replikation von der alten zur neuen Datenbank (die ein paar Tage vor dem Cutover läuft) die neue Instanz schrittweise aufholen und reduziert die Cutover-Zeit auf Sekunden:
# Auf dem Shared Host (falls SSH-Zugriff verfuegbar ist)
mysqldump --single-transaction --master-data=2 \
--databases app_production > app_dump.sql
# Auf der neuen Managed Database
mysql -h new-db.example.com -u admin -p < app_dump.sql
# Replikation konfigurieren, falls der Cutover nahezu sofort erfolgen muss
# (anbieterspezifisch; Managed-DB-Dokumentation konsultieren)
Für Shared Hosts, die keinen SSH-Zugriff erlauben (häufig bei Plesk und cPanel), verwende die nativen Backup-Tools des Control-Panels oder die Datenbank-Export-Schnittstelle. Verifiziere den Import, indem du repräsentative Abfragen der Anwendung gegen die neue Datenbank ausführst, bevor irgendein Produktionsverkehr involviert ist.
Schritt vier: CI/CD, das FTP-Deployment ersetzt
Die wirkungsvollste einzelne Änderung in dieser Migration ist, manuelle FTP-Deploys durch ein automatisiertes, von Git gesteuertes CI/CD-Setup zu ersetzen. Sobald der Code in Versionskontrolle lebt und in Containern läuft, wird Deployen zur Sache, ein Image zu bauen, es in eine Registry zu pushen und den Produktionsserver anzuweisen, die neue Version zu ziehen.
GitHub Actions ist für die meisten Teams die pragmatische Wahl, weil es sich in das Git-Repository integriert, in dem die Codebasis bereits lebt. Ein minimaler Workflow für eine dockerisierte PHP-Anwendung:
# .github/workflows/deploy.yml
name: Build and Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Log in to container registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/company/legacy-app:${{ github.sha }}
- name: Trigger deployment
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.PROD_HOST }}
username: deploy
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
cd /opt/app
export IMAGE_TAG=${{ github.sha }}
docker compose pull
docker compose up -d --no-deps app
docker image prune -f
Diese Pipeline läuft in wenigen Minuten, erzeugt ein mit dem Commit-SHA getaggtes Docker-Image und deployt es ohne manuelle Eingriffe auf den Produktionsserver. Rollbacks werden zur Ein-Zeilen-Operation: per SSH auf den Server, IMAGE_TAG auf den SHA des vorherigen Commits setzen und erneut docker compose up -d ausführen.
Die Disziplin, die dies erzwingt, ist beträchtlich. Jede Änderung lebt nun in einem Git-Commit mit Nachricht und Autor. Jedes Deployment ist aus dem Commit reproduzierbar, der es erzeugt hat. Mehrere Entwickler können an der Codebasis arbeiten, ohne sich gegenseitig zu überschreiben. Das Team, das zuvor jede Produktionsänderung fürchtete, liefert nun mehrmals pro Woche aus.
Schritt fünf: DNS-Cutover mit definiertem Rollback-Fenster
Der letzte Schritt ist, den Produktionsverkehr vom Shared-Hosting-Konto auf die neue Infrastruktur zu verlagern. Dies ist der einzige Schritt, der sich wirklich nicht teilweise durchführen lässt. Irgendwann muss der DNS-Eintrag auf den neuen Server zeigen.
Der Cutover-Plan, der das Risiko minimiert, hat vier Elemente. Erstens: Senke die DNS-TTL des Anwendungs-Hostnamens mindestens 48 Stunden vor dem Cutover auf 300 Sekunden (5 Minuten), damit DNS-Änderungen schnell propagieren und ein Rollback schnell geht. Zweitens: Plane den Cutover für die verkehrsärmste Stunde der Woche, die für die meisten europäischen B2B-Anwendungen sonntags zwischen 02:00 und 04:00 Uhr MEZ liegt. Drittens: Halte beide Umgebungen mindestens 48 Stunden nach dem Cutover parallel am Laufen, mit dem Shared Host warmgehalten und bereit, bei Bedarf erneut Verkehr zu empfangen. Viertens: Überwache Fehlerraten, Antwortzeiten und Datenbankverbindungen kontinuierlich während und nach dem Cutover.
Wenn die neue Datenbank als Replik aufgesetzt wurde, stoppe die Replikation und befördere sie im Cutover-Moment zur Primärdatenbank, dann ändere den DNS-Eintrag. Wenn die Datenbank per mysqldump migriert wurde, nimm dir ein kurzes Wartungsfenster (typischerweise 15 bis 30 Minuten) für einen finalen Sync, dann führe den Cutover durch.
Die Rollback-Bedingung muss im Voraus definiert werden. Ein typischer Schwellenwert ist "wenn die 5xx-Fehlerrate für mehr als 10 Minuten 1 % übersteigt oder sich die mediane Antwortzeit mehr als verdoppelt, sofort auf den Shared Host zurückrollen". Dies vor dem Cutover zu definieren verhindert die panische "Sollen wir zurückrollen?"-Debatte um 03:00 Uhr, wenn das Urteilsvermögen am schlechtesten ist.
Häufige Fehler bei Shared-Hosting-Migrationen
Der mit Abstand häufigste Fehler ist, PHP zur selben Zeit zu aktualisieren, in der die Infrastruktur verlagert wird. Gleichzeitig von Shared Hosting zu Docker und von PHP 7.4 zu PHP 8.3 zu migrieren, kombiniert zwei unabhängige Risiken zu einem Ereignis. Tue es nacheinander: Containerisiere zuerst auf der bestehenden PHP-Version, führe den Cutover auf die neue Infrastruktur durch, aktualisiere dann PHP als separate, kleinere Migration mit eigenem Testzyklus.
Der zweite häufige Fehler ist, die Migration als den Moment zu behandeln, alles zu reparieren. Inline-Zugangsdaten, fehlende Tests, die doppelten admin_old/-Ordner, die Funktion namens final_v3_real_use_this(): All das sind echte Probleme, die es verdienen, angegangen zu werden. Keines davon gehört in den Migrations-Commit. Nimm dir vor, sie zu beheben, nachdem die Anwendung auf moderner Infrastruktur läuft, wo jeder Fix in Minuten deployt und zurückgerollt werden kann, falls er etwas zerstört.
Der dritte häufige Fehler ist, vor dem Cutover zu wenig in Observability zu investieren. Der Shared Host hatte wahrscheinlich kein echtes Monitoring über Uptime-Checks hinaus. Die neue Infrastruktur sollte mit strukturiertem Logging (Monolog, das JSON schreibt), grundlegenden Metriken (Server-CPU, Speicher, Antwortzeiten) und Uptime-Monitoring von mindestens einem externen Standort ausgeliefert werden. Ohne dies bleibt ein Problem nach dem Cutover unsichtbar, bis Kunden es melden.
Eine fokussierte Optimierung von Legacy-Code vor Beginn der Migration identifiziert typischerweise die meisten dieser Fallen im Voraus und erzeugt einen sequenzierten Plan, der sie vermeidet.
Was Modernisierung freisetzt
Der strategische Wert dieser abgeschlossenen Migration ist nicht die neue Infrastruktur selbst, sondern die Veränderungen, die die neue Infrastruktur möglich macht. Mit der Anwendung in Git und per CI/CD deployt wird das Einstellen eines zweiten Entwicklers ohne betriebliches Chaos machbar. Mit Docker ist das Erstellen einer Staging-Umgebung eine Sache, dasselbe Image gegen eine separate Datenbank laufen zu lassen. Mit verwalteten Backups und Point-in-Time-Recovery kann der Gründer aufhören, sich um das Szenario eines Festplattenausfalls um 03:00 Uhr zu sorgen.
Diese Fähigkeiten ermöglichen dann die nächste Arbeitsphase: inkrementelles Refactoring, Framework-Adoption (oft Symfony für PHP-Anwendungen dieser Größe), Testabdeckung kritischer Geschäftslogik und schließlich die Art von Feature-Geschwindigkeit, die das Unternehmen seit dem Start nicht mehr gesehen hat. Nichts davon ist möglich, solange die Anwendung noch auf Shared Hosting lebt. Alles davon wird zur Routine, sobald die Migration abgeschlossen ist.
Wenn dein Unternehmen eine umsatzgenerierende PHP-Anwendung auf Shared Hosting betreibt und du das Risiko der Modernisierung gegen die Kosten des Weiterbetriebs abwägst, bietet Wolf-Tech eine strukturierte Migrationsbewertung, die einen sequenzierten, risikoarmen Plan erzeugt, der auf die spezifischen Abhängigkeiten und Rahmenbedingungen deiner Anwendung zugeschnitten ist. Kontaktiere uns unter hello@wolf-tech.io oder besuche wolf-tech.io für eine kostenlose Erstberatung.
