Alle Artikel
12. September 2026
9 mins

Ein PHP-Monolith auf ECS Fargate

Von Tim Rutte, Cloud & Software ArchitectThemaAWS & Cloud

Ein einzelner weißer Frachtcontainer auf glatter Fläche, seine Verriegelungsstangen sind blau.

Wenn eine PHP-Anwendung von einem Server in die Cloud zieht, steht früh die Frage nach Containern im Raum, und kurz darauf die nach Kubernetes. Für einen einzelnen Monolithen mit einem Team dahinter ist das in den meisten Fällen zu viel, und es verzögert den Umzug um Monate.

Dieser Artikel beschreibt den kleineren Weg: einen PHP-Monolithen in Container packen und auf ECS Fargate betreiben. Ohne eigene Maschinen, ohne Cluster-Verwaltung, ohne ein zweites Betriebsmodell. Das reicht für erstaunlich viele Systeme, und es lässt die Tür zu allem Weiteren offen.

Voraussetzung ist, dass die Anwendung keinen Zustand mehr auf ihrer Festplatte hält. Solange Sitzungen und hochgeladene Dateien lokal liegen, ist der Umzug in Container ein Umzug der Probleme.

Warum Fargate und nicht Kubernetes

Die ehrliche Begründung ist keine technische Überlegenheit, sondern eine Rechnung über Betriebsaufwand.

Kubernetes löst Probleme, die ein einzelner Monolith nicht hat: viele Dienste, viele Teams, eigene Netzregeln zwischen Diensten, Bedarf an einheitlichen Abläufen über verschiedene Sprachen. Dafür ist es gut, und dafür lohnt es sich. Der Preis ist ein zweites System, das betrieben, aktualisiert und verstanden werden muss, und dieser Preis fällt unabhängig davon an, ob man die Vorteile braucht.

Fargate lässt genau diesen Teil weg. Es gibt keine Maschinen, die jemand pflegt: Man beschreibt einen Container, sagt, wie viele davon laufen sollen, und das war es. Was fehlt, sind die feineren Einstellungen, und für einen Monolithen braucht man sie meist nicht.

Die Regel, die ich daraus ziehe: Kubernetes lohnt sich ab dem Punkt, an dem mehrere Teams unabhängig voneinander ausrollen wollen. Vorher ist es Aufwand, der sich als Vorbereitung tarnt. Und der Weg von Fargate dorthin ist später offen, weil das Container-Image dasselbe bleibt.

Die Voraussetzung dafür ist erledigt, bevor das erste Abbild gebaut wird: Sessions, Uploads, Konfiguration.

Das Image: was hinein gehört und was nicht

Ein PHP-Image für einen Monolithen ist unspektakulär, und die Fehler passieren an immer denselben drei Stellen.

# Bauen und Laufen trennen: das Ergebnis enthaelt keinen Composer,
# keine Entwicklungsabhaengigkeiten und keine Quellen der Oberflaeche.
FROM composer:2 AS abhaengigkeiten
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --optimize-autoloader

FROM php:8.3-fpm-alpine
RUN docker-php-ext-install pdo_mysql opcache bcmath \
 && apk add --no-cache fcgi

COPY --from=abhaengigkeiten /app/vendor ./vendor
COPY . .

# Opcache-Einstellungen fuer den Betrieb: die Dateien aendern sich im
# Container nie, also muss PHP nicht bei jedem Aufruf nachsehen.
COPY docker/opcache.ini /usr/local/etc/php/conf.d/

USER www-data

Erstens: das Image enthält keine Geheimnisse. Keine .env, keine Zugangsdaten, keine Zertifikate. Ein Image landet in einem Verzeichnis, aus dem es viele lesen können, und es bleibt dort jahrelang liegen.

Zweitens: das Image enthält keinen Zustand. Kein Verzeichnis für hochgeladene Dateien, keine Sitzungen, keine Zwischenspeicher, die etwas Wichtiges enthalten. Container werden ersetzt, und was in ihnen liegt, ist dann weg.

Drittens: das Image läuft nicht als Wurzelbenutzer. Eine Zeile, die in der ersten Fassung fast immer fehlt und später schwer nachzurüsten ist, weil Dateirechte daran hängen.

Dazu ein Detail, das häufig übersehen wird: In einem Container ändern sich die Dateien nie. Deshalb kann der Opcache die Prüfung auf Änderungen abschalten, was messbar Aufrufe spart. Auf einem Server war das gefährlich, hier nicht.

Konfiguration und Geheimnisse

Wenn die .env nicht ins Image darf, muss die Konfiguration von außen kommen. ECS unterscheidet dabei zwei Wege, und die Unterscheidung ist keine Formsache.

"environment": [
  { "name": "APP_ENV", "value": "prod" },
  { "name": "DB_HOST", "value": "db.intern" }
],
"secrets": [
  { "name": "DB_PASSWORT",
    "valueFrom": "arn:aws:secretsmanager:eu-central-1:...:geheimnis/db" }
]

Der Unterschied: Was unter environment steht, ist in der Aufgabenbeschreibung sichtbar, für jeden mit Lesezugriff und in jeder Protokollausgabe, die Umgebungsvariablen ausgibt. Was unter secrets steht, wird beim Start geholt und steht nirgends im Klartext.

Die Regel dazu ist einfach: Alles, womit man sich irgendwo anmelden kann, gehört unter secrets. Alles andere unter environment, weil es dort lesbar und damit nachvollziehbar ist.

Für eine bestehende PHP-Anwendung, die eine .env erwartet, ist der kleinste Weg meist ein Startskript, das die Datei aus den Umgebungsvariablen schreibt. Das ist keine schöne Lösung und sie erspart einen Umbau der Konfigurationsschicht mitten im Umzug. Sie bekommt ein Datum, wie jede Zwischenlösung.

Protokolle: von Dateien zu der Ausgabe

Eine Anwendung, die in Dateien protokolliert, verliert ihre Protokolle im Container, sobald er ersetzt wird. Die Umstellung ist klein und gehört zum Umzug.

// Monolog: Ausgabe statt Datei, und in einem Format, das sich
// filtern laesst. JSON, damit die Suche nicht auf Text raten muss.
$log = new Logger('app');
$handler = new StreamHandler('php://stdout', Level::Info);
$handler->setFormatter(new JsonFormatter());
$log->pushHandler($handler);

// Zusaetzlich in jede Zeile: die Kennung der Anfrage. Ohne sie sind
// die Zeilen von acht Containern nicht auseinanderzuhalten.
$log->pushProcessor(fn (LogRecord $r) => $r->with(extra: [
    'anfrage' => $_SERVER['HTTP_X_AMZN_TRACE_ID'] ?? '-',
]));

Der zweite Teil ist der wichtigere. Sobald mehrere Container laufen, sind die Protokollzeilen ineinander verwoben, und ohne eine Kennung je Anfrage ist eine Fehlersuche nicht mehr möglich. Der Lastverteiler liefert eine, man muss sie nur durchreichen.

Was dabei auffällt: PHP-FPM schreibt eigene Zeilen, der Webserver auch. Drei Quellen in einem Container sind in Ordnung, solange alle in die Ausgabe schreiben und erkennbar ist, welche Quelle eine Zeile hat.

Der Gesundheitscheck, und warum er mehr prüfen muss als „läuft"

ECS ersetzt Container, die als ungesund gemeldet werden, und der Lastverteiler nimmt sie aus dem Verkehr. Beides ist nur so gut wie die Prüfung dahinter.

Der erste Reflex ist eine Seite, die OK zurückgibt. Die prüft, ob PHP läuft, und mehr nicht. Ein Container, dessen Datenbankverbindung abgerissen ist, gilt damit als gesund und bekommt weiter Anfragen.

// /gesundheit: prueft die Abhaengigkeiten, die eine Anfrage braucht,
// und nichts darueber hinaus.
public function gesundheit(): Response
{
    try {
        $this->db->query('SELECT 1');
        $this->cache->get('gesundheit');
    } catch (Throwable $e) {
        // 503, damit der Lastverteiler diesen Container aus dem Verkehr
        // nimmt, statt Anfragen dagegen laufen zu lassen.
        return new Response(503, ['zustand' => 'fehler']);
    }

    return new Response(200, ['zustand' => 'ok']);
}

Zwei Grenzen dazu, beide aus Erfahrung. Die Prüfung ruft keine Fremdsysteme auf. Wenn der Zahlungsdienstleister ausfällt, sollen nicht alle Container als ungesund gelten und ersetzt werden; das macht aus einem Teilausfall einen ganzen. Die Prüfung ist günstig. Sie läuft alle fünfzehn Sekunden je Container, und eine Prüfung, die eine Datenbankabfrage über drei Tabellen macht, wird bei zwanzig Containern selbst zur Last.

Rollout und Rückweg

Der Rollout ist der Teil, der ohne Zutun besser wird als vorher. ECS ersetzt Container schrittweise: Neue starten, werden geprüft, bekommen Verkehr, alte werden abgeräumt.

Drei Einstellungen entscheiden darüber, ob das im Betrieb ruhig aussieht.

Die Anlaufzeit im Gesundheitscheck. Ein PHP-Container ist in zwei Sekunden bereit, aber wenn beim Start Zwischenspeicher aufgebaut werden, dauert es länger. Ist die Anlaufzeit zu kurz, ersetzt ECS Container, die nur noch nicht fertig waren, und tut das in einer Schleife.

Die Abräumfrist. Ein Container, der aus dem Verkehr genommen wird, hat noch Anfragen in Arbeit. Die Frist muss länger sein als die längste normale Anfrage, sonst bricht bei jedem Rollout eine Handvoll Vorgänge ab.

Die Zahl der gleichzeitig laufenden Fassungen. Solange alte und neue Container nebeneinander laufen, müssen beide mit demselben Datenbankstand arbeiten. Das ist dieselbe Bedingung wie bei jedem Rollout ohne Unterbrechung, und sie ist der Grund, warum Schemaänderungen in zwei Schritten kommen.

Der Rückweg ist die vorherige Aufgabenbeschreibung, und das ist der angenehmste Teil des ganzen Modells: Sie liegt noch da, und ein Rückschwenk dauert so lange wie ein Rollout.

Wie ein Ausrollen ohne Ausfall auch ohne Orchestrierung aussieht, steht in Blue/Green und Canary ohne Kubernetes.

Was danach anders ist, auch auf der Rechnung

Nach dem Umzug ändert sich der Betrieb an drei Stellen, und nicht alle davon sind billiger.

Die Pflege der Maschinen entfällt. Betriebssystem, Aktualisierungen, Plattenplatz: Diese Arbeit verschwindet. Für kleine Teams ist das der eigentliche Gewinn, größer als jede technische Eigenschaft.

Die Anwendung ist mehrfach vorhanden. Was vorher ein Server war, sind jetzt mindestens zwei Container in zwei Zonen. Damit ist der Ausfall einer Zone kein Vorfall mehr. Das kostet aber auch: Zwei kleine Container sind teurer als einer, der doppelt so groß ist.

Die Rechnung wird berechenbar und nicht automatisch kleiner. Fargate kostet nach zugeteiltem Speicher und Rechenzeit, unabhängig von der Auslastung. Wer die alte Serverdimensionierung übernimmt, zahlt mehr als vorher. Der Gewinn entsteht erst, wenn die Zuteilung an die gemessene Last angepasst wird, und das ist eine eigene Aufgabe nach dem Umzug.

Genau deshalb gehört die Messung vor die Umstellung: Wer die Antwortzeiten und die Lastspitzen des alten Betriebs kennt, kann die Zuteilung begründen statt zu raten. Wie sich eine Cloud-Rechnung über zwölf Monate entwickelt, wenn man sie ernst nimmt, steht in einem eigenen Artikel.

Wie eine AWS-Migration insgesamt abläuft, steht auf einer eigenen Seite; der Schritt in Container ist dort einer von mehreren und nicht der erste.

Dieser Artikel gehört zu einer Reihe über Systeme, die es schon gibt. Der Rückblick ordnet alle Artikel der Reihe nach Anlass.