Ich zeige dir die produktionsreife Konfiguration für PHP Error Handling Hosting: von php.ini-Defaults über Logging-Strategien bis hin zu Custom-Handlern für saubere Responses. So halte ich production errors aus der Oberfläche fern, sichere sensible Informationen und erhöhe die server stability im Live-Betrieb.
Zentrale Punkte
- php.ini trennen: DEV zeigt alles, PROD loggt diskret.
- Error-Level fein filtern: Fokus auf echte Produktionsfehler.
- Custom-Handler nutzen: Fehler abfangen, sauber reagieren.
- Logging strukturieren: Kontext, Rotation, Alerts.
- Umgebungen klar trennen: DEBUG-Flags und sichere Defaults.
Produktionsreife PHP-Fehlerkonfiguration kurz erklärt
In der Entwicklung lasse ich alle Meldungen erscheinen, weil ich Codequalität früh sichere. Auf Live-Servern drehe ich die Anzeige strikt ab, protokolliere jedoch alles, damit ich Diagnose jederzeit möglich mache. So bleiben Anwenderoberflächen sauber, während Logs die Wahrheit sagen. Sichtbare Fehltexte gefährden Vertraulichkeit und können Funktionsketten unterbrechen; ich verhindere das mit klarer Trennung. Dieses Muster steigert die server stability und hält die Reaktionszeiten planbar.
php.ini: sichere Defaults für Live-Traffic
Für Entwicklungsumgebungen aktiviere ich display_errors und setze error_reporting auf E_ALL oder -1. In der Produktion schalte ich Anzeigen konsequent aus, behalte aber umfassendes Reporting und Logging bei. Der Mix schützt Benutzer und hält meine Einsicht in das Systemverhalten intakt. Ich lege die Werte zentral in php.ini fest und versioniere ergänzende ini-Snippets. So bekomme ich reproduzierbare Deployments und reduzierte Überraschungen im Live-Betrieb.
Die folgende Tabelle zeigt die Gegenüberstellung typischer DEV- und PROD-Settings für mehr Transparenz und klare Leitlinien:
| Einstellung | Development | Production | Hinweis |
|---|---|---|---|
| display_errors | On | Off | Anzeige im Live-Betrieb strikt vermeiden |
| display_startup_errors | On | Off | Startfehler nur in DEV sichtbar machen |
| error_reporting | E_ALL oder -1 | E_ALL (optional filtern) | -1 deckt alle Level inkl. zukünftiger ab |
| log_errors | On | On | Logs sind Pflichtquelle für Analyse |
| error_log | Datei/Pfad | Datei/Pfad | Pfad mit Rotation und Rechten absichern |
Ich setze in PROD also “anzeigen aus, melden an” und lasse via error_log alles in Dateien schreiben. Zusätzlich harte ich Dateirechte, weil Logfiles oft sensible Kontexte enthalten. Wer Virtual Hosts oder Container nutzt, trennt Pfade sauber pro Anwendung. Das vereinfacht spätere Korrelationen und beschleunigt die Ursachenanalyse. So bleibt die Oberfläche freundlich, während ich im Hintergrund vollständige Spuren erhalte.
Error-Reporting-Level feinjustieren ohne Log-Flut
Standardmäßig nutze ich in PROD E_ALL und filtere optional Nebengeräusche wie Notices, wenn sie keinen Wert stiften. Ein häufig genutztes Muster lautet E_ALL & ~E_NOTICE & ~E_WARNING & ~E_DEPRECATED. Das verhindert Rauschen, fokussiert aber weiter auf echte production errors. Vor Änderungen prüfe ich Auswirkungen auf Durchsatz und Latenz, weil viel Logging IO kostet. Wer die Effekte je Level verstehen will, findet Hintergründe zu Fehlerlevel und Performance.
Ich halte das Prinzip “erst sauber fixen, dann filtern” hoch, da Verdrängen nur Probleme verschiebt. Für Migrationsphasen lasse ich DEPRECATED sichtbar loggen, um künftige Brüche früh zu erkennen. Zusätzlich markiere ich kritische Fehlerklassen separat, damit Alarme zuverlässig feuern. So profitieren Analysewege und ich spare Zeit in der Störungsbehebung. Das Resultat ist weniger Rauschen und mehr verwertbare Signale.
Custom-Handler: Exceptions, Errors und Shutdown sauber abfangen
Ich installiere eigene Handler mit set_error_handler(), set_exception_handler() und register_shutdown_function(). So fange ich klassische Fehler, ungefangene Exceptions und Fatal Errors ab. Für Anwender liefere ich eine neutrale 500-Seite, im Log landet der vollständige Kontext. Das schützt sensitive Details und hält die server stability hoch. Gleichzeitig behalte ich die Hoheit über Format, Felder und Ausgabekanäle.
<?php
class ErrorHandler {
public static function register() {
set_error_handler([__CLASS__, 'handleError']);
set_exception_handler([__CLASS__, 'handleException']);
register_shutdown_function([__CLASS__, 'handleShutdown']);
}
public static function handleError($errno, $errstr, $errfile, $errline) {
error_log("ERROR: [$errno] $errstr in $errfile on line $errline");
if ($errno === E_ERROR) {
http_response_code(500);
echo "Ein interner Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.";
}
return true;
}
public static function handleException($exception) {
error_log("EXCEPTION: " . $exception->getMessage());
http_response_code(500);
echo "Ein interner Fehler ist aufgetreten.";
}
public static function handleShutdown() {
$error = error_get_last();
if ($error !== null) {
error_log("FATAL: " . $error['message']);
http_response_code(500);
}
}
}
ErrorHandler::register(); Im Alltag ergänze ich Felder wie Request-ID, User-ID und Session-Hash, um Korrelation zu erleichtern. Für APIs liefere ich in PROD eine generische Fehlstruktur, etwa JSON mit Code und Ticket-ID. So kann Support sofort ansetzen, während Interna verborgen bleiben. Zusätzlich kapsle ich IO rund um Logger, damit ein defektes Filesystem nicht weitere Fehler auslöst. Diese Kaskadenvermeidung zahlt direkt auf geringere MTTR ein.
Strukturiertes Logging: Kontext, Rotation, Alerts
Gutes Logging beginnt mit Kontext: Zeitstempel, Typ, Datei, Zeile und Request-Bezug. Danach folgt die Disziplin: Rotationspolitik, Rechte und Aufbewahrung. Ich trenne App-Logs und Webserver-Logs, um schnelle Übersicht zu behalten. Kritische Klassen wie E_ERROR löse ich in Alarmkanälen aus, etwa Mail oder Chat. Laut blog.nevercodealone.de reduziert ein klares Fehlerprotokoll die Debug-Zeit um bis zu 70 % – ein starker Hebel für Operations.
<?php
set_error_handler(function ($errno, $errstr, $errfile, $errline) {
if (!(error_reporting() & $errno)) return false;
$type = match($errno) {
E_ERROR => 'ERROR',
E_WARNING => 'WARNING',
E_NOTICE => 'NOTICE',
default => 'UNKNOWN'
};
$message = sprintf(
"[%s] %s: %s in %s on line %d | req=%s user=%s",
date('Y-m-d H:i:s'), $type, $errstr, $errfile, $errline,
$_SERVER['HTTP_X_REQUEST_ID'] ?? '-', $_SESSION['uid'] ?? '-'
);
error_log($message, 3, '/var/log/app/custom_error.log');
if ($errno === E_ERROR) {
// Alert versenden
}
return true;
}); Ich prüfe Log-Größe täglich oder automatisiert, um Speicher zu schonen. Rotation mit size- oder time-basierten Regeln verhindert volle Platten. Zusätzlich schreibe ich optional im JSON-Format, damit Parser Metriken extrahieren. Für die Auswertung hilft ein strukturierter Einstieg; hier liefert der Leitfaden zu Logs analysieren nützliche Denkanstöße. So erkenne ich Ausreißer schneller und minimiere Blindflug.
Trennung von DEV, STAGE und PROD konsequent leben
Ich halte jede Umgebung mit eigenem DEBUG-Flag und dedizierten ini-Overrides getrennt. Konfigurationswerte landen in Env-Variablen, nicht im Code. Der Webserver zeigt in PROD Cache-Header, während DEV großzügig deaktiviert. Für STAGE spiegele ich PROD-Einstellungen, aktiviere aber zusätzliche Metriken. Diese Disziplin verhindert Überraschungen und erhöht die Vorhersagbarkeit von Deployments.
Namen von Logfiles unterscheiden sich je Umgebung, damit ich Fehlerbilder nicht vermische. CI/CD setzt die Flags vor dem Rollout, sodass kein Menschuellfehler hineinrutscht. Ich ergänze Health-Checks für wesentliche Endpunkte, damit Downtimes früh auffallen. Feature-Flags helfen, riskante Pfade temporär abzuschirmen. So halte ich Releases kalkulierbar und senke Rollback-Risiken.
Runtime-Debugging: Wenn ich schnell prüfen muss
Manchmal brauche ich kurzzeitig Einblick auf einer Test-Instanz, etwa unmittelbar nach einem Hotfix. Dann setze ich temporär ini_set(‚display_errors‘, 1) und error_reporting(E_ALL) – jedoch niemals auf echter Produktion. Ich protokolliere jede Änderung, lösche sie danach und committe nichts davon. Eine kurze Prüfrunde mit gezielten Requests reicht meist aus. Danach kehre ich sofort zu stillen Logs und neutralen Fehlseiten zurück.
Für reproduzierbare Analysen kapsle ich Debug-Flags hinter Feature-Toggles, die ich zeitlich limitiere. So verhindere ich Dauerzustände und reduziere Risiko. Wenn ich tiefer graben muss, setze ich auf Xdebug in einer isolierten DEV-Umgebung. Messen statt Raten bleibt dabei das Leitmotiv. Nur so erkenne ich reale Flaschenhälse und keine Placebos.
WordPress und Frameworks sicher konfigurieren
Bei WordPress setze ich in PROD WP_DEBUG auf false und leite Fehler in Logs um. In DEV nutze ich WP_DEBUG_LOG und WP_DEBUG_DISPLAY gezielt für Feature-Entwicklung. Ich deaktiviere Plugin-Editoren in PROD, damit keine Codeänderungen live passieren. Die Cron-Steuerung über System-Cronjobs reduziert Ausreißer und glättet Lastspitzen. Für Details hilft der kompakte Leitfaden zum WordPress Debug‑Modus.
Frameworks wie Symfony oder Laravel liefern dedizierte ENV-Flags und Error-Pages, die ich konsequent nutze. Ich setze zentralisierte Logger wie Monolog mit Channel-Struktur ein. Für HTTP-Antworten in PROD gebe ich generische Fehlertexte aus und verweise intern auf Korrelationen. So bleiben Oberflächen neutral, Logs aber ergiebig. Diese Kombination trägt spürbar zur server stability bei.
Security-Aspekte: Was niemals im Log landen darf
Ich filtere konsequent Geheimnisse: Passwörter, Tokens, Kreditkartenfragmente und personenbezogene Daten. Maskierung erfolgt möglichst früh, zum Beispiel auf Service-Ebene vor dem Logger. Für Fehlermeldungen prüfe ich, ob Inhalte Dateipfade, SQLs oder interne IPs enthalten. Alles, was Angriffsflächen vergrößert, schirme ich ab oder anonymisiere ich. So bleiben Logs nützlich, ohne Datenschutz oder Sicherheit zu gefährden.
Dateirechte setze ich restriktiv, und Prozesse schreiben nur in freigegebene Pfade. Zusätzlich aktiviere ich Logrotation mit Komprimierung, damit alte Daten nicht offen herumliegen. Für Vorfälle halte ich ein Runbook bereit: Wo finde ich welche Spuren, welche Teams benachrichtige ich zuerst. Diese Vorbereitung spart kostbare Minuten in hektischen Lagen. Am Ende zählt die Zeit bis zur Wiederherstellung.
Monitoring und Alarmierung ohne Fehlzündungen
Ich definiere Schwellwerte, die kontextsensitiv sind: Einzelne Warnings triggern keinen Alarm, plötzliche Peaks sehr wohl. Zeitfenster, Rate-Limits und Deduplizierung verhindern Pager-Müdigkeit. Kritische Klassen wie E_ERROR, E_PARSE und wiederkehrende Zeitüberschreitungen melde ich unmittelbar. Für wiederkehrende Ausreißer plane ich Tickets statt Ad-hoc-Maßnahmen. So bleibt das Team handlungsfähig, und echte Probleme gehen nicht unter.
Visualisierung hilft mir, Muster zu erkennen: Tageszyklen, Deploy-Spitzen, Bot-Wellen. Korrelationen zwischen Release-Zeitpunkten und Fehlerraten decken Ursachen auf. Ich hinterlege Runbooks direkt in Alarmtexten, damit On-Call sofort handeln kann. Abhängigkeiten wie Datenbanken und Queues überwache ich ebenfalls. Ein Fehlerstrom ohne Kontext liefert selten Lösungen.
Deployment-Checkliste: Fehlerarm ausrollen
Vor jedem Rollout prüfe ich Konfiguration, Logs, Rechte und freien Speicher. Danach führe ich einen Smoke-Test mit den wichtigsten Endpunkten durch. Feature-Flags und Canary-Releases verringern Risiken bei größeren Änderungen. Ich protokolliere Deploy-Zeiten, um Korrelationen später zu erleichtern. Außerdem plane ich Rückwege, falls ein Hotfix schiefgeht.
Für größere Updates verlagere ich Schreiblast kurzzeitig und führe Readiness-Probes strenger aus. Dazu zählt ein Check auf Logschreibbarkeit und Datenbank-Verbindungen. Zusätzlich kontrolliere ich, ob 500-Seiten korrekt und ohne Interna erscheinen. Diese scheinbar kleinen Punkte verhindern große Überraschungen. Rollouts werden dadurch leiser und nachvollziehbarer.
FPM und Webserver: SAPI-spezifisch absichern
Neben php.ini sichere ich die FPM-Pools hart ab. Pool-weit setze ich display_errors per php_admin_flag auf Off und erzwinge damit produktive Defaults selbst bei fehlerhaften Applikations-Overrides. Mit slowlog und request_terminate_timeout identifiziere und begrenze Hänger, bevor sie Worker blockieren. Zusätzlich protokolliere ich Worker-Ausgaben, um seltene Edge Cases festzuhalten.
[www]
php_admin_flag[display_errors] = Off
php_admin_value[error_reporting] = E_ALL
php_admin_value[log_errors] = On
php_admin_value[memory_limit] = 256M
catch_workers_output = yes
request_terminate_timeout = 30s
slowlog = /var/log/php-fpm/www-slow.log
request_slowlog_timeout = 5s Auf Webserver-Ebene (nginx/Apache) aktiviere ich fastcgi_intercept_errors bzw. ProxyErrorOverride. So kann der Webserver statische 50x-Seiten ausliefern, wenn PHP ausfällt. Ich cache keine 5xx-Antworten, versehe aber 4xx-Fehler mit kurzen TTLs. Ein X-Request-ID-Header wird vom Webserver generiert und an PHP durchgereicht, damit ich jeden Pfad korreliere.
# nginx
error_page 500 502 503 504 /50x.html;
location = /50x.html { root /usr/share/nginx/html; internal; }
fastcgi_intercept_errors on;
add_header X-Request-Id $request_id always;
# Apache (Auszug)
ErrorDocument 500 /50x.html
ProxyErrorOverride On In PROD deaktiviere ich außerdem html_errors und expose_php. Das verhindert HTML-formatierte Fehltexte und Leaks über PHP-Versionen. Mit ignore_repeated_errors und log_errors_max_len halte ich Logstürme im Zaum, ohne echte Signale zu verschlucken. Opcache betreibe ich strikt produktionsnah, achte aber darauf, dass Fehlermeldungen nicht durch aggressive Revalidation verdeckt werden.
Einheitliche Fehler-Responses für APIs und Frontends
Ich standardisiere das Antwortschema: Benutzer sehen generische Texte, Systeme erhalten strukturierte Codes. 4xx-Fehler signalisieren Client-Probleme (Validation, Auth), 5xx-Fehler stehen für Serverthemen. Eine konsistente Abbildung von Exceptions auf HTTP-Status verhindert Missverständnisse und erleichtert Monitoring.
<?php
function respondError(int $status, string $code, string $publicMessage, array $meta = []): void {
http_response_code($status);
$payload = [
'error' => [
'code' => $code,
'message' => $publicMessage,
'request_id' => $_SERVER['HTTP_X_REQUEST_ID'] ?? '-',
'timestamp' => date('c'),
] + $meta
];
header('Content-Type: application/json');
echo json_encode($payload);
}
try {
// ...
} catch (ValidationException $e) {
respondError(422, 'VALIDATION_FAILED', 'Eingabe unvollständig oder ungültig');
} catch (NotFoundException $e) {
respondError(404, 'NOT_FOUND', 'Ressource nicht gefunden');
} catch (Throwable $e) {
error_log('UNHANDLED: '.$e->getMessage());
respondError(500, 'INTERNAL_ERROR', 'Ein interner Fehler ist aufgetreten');
} Für UIs halte ich eine saubere 500-Seite bereit, die keine Interna zeigt. Lokalisiere ich Fehltexte, tue ich das ausschließlich für öffentliche Nachrichten – interne Details bleiben in Logs. Das steigert Qualität bei Support und reduziert Rückfragen.
Zentrale Log-Sammlung, Sampling und Container
In modernen Setups leite ich Logs zentral an Syslog oder Journald weiter. In Containern schreibe ich bevorzugt nach stdout/stderr und überlasse Rotation und Versand der Plattform. File-basierte Logs in Containern vermeide ich, es sei denn, ein Sidecar dreht sie verlässlich. Sampling nutze ich kontrolliert: Bei massenhaften gleichartigen Warnungen zeichne ich repräsentative Stichproben auf und hebe weiterhin jede kritische Klasse vollumfänglich auf.
Ich enrichere Logzeilen um Deployment-Hash, Host, Pod/Container-ID und Umgebung. Fällt der zentrale Versand aus, buffer ich lokal begrenzt und falle notfalls auf Minimal-Logging zurück, um den Request nicht zu blockieren. Netzwerkprobleme dürfen keine Fehlerkaskaden im kritischen Pfad auslösen – Stabilität geht vor Vollständigkeit.
CLI, Cronjobs und Worker-Prozesse robust behandeln
CLI-Skripte folgen eigenen Regeln: Sie brauchen Exit-Codes, schreiben nach STDERR und dürfen nie stumm scheitern. Ich trenne ihre Logs von Web-Requests und sorge für Backoff/Retry-Strategien bei transienten Fehlern. Für lange Jobs setze ich Memory-Limits bewusst und protokolliere Zwischenstände, damit ich Hänger oder Leaks erkenne.
<?php
if (PHP_SAPI === 'cli') {
set_error_handler(function($errno, $errstr, $errfile, $errline) {
$msg = sprintf("CLI [%s] %s in %s:%d\n", $errno, $errstr, $errfile, $errline);
fwrite(STDERR, $msg);
return true;
});
register_shutdown_function(function() {
$e = error_get_last();
if ($e) fwrite(STDERR, "CLI FATAL: {$e['message']}\n");
});
}
try {
// Job-Logik
exit(0);
} catch (Throwable $e) {
fwrite(STDERR, "CLI EXCEPTION: ".$e->getMessage()."\n");
// 2 = temporär, 1 = dauerhaft, 3 = Konfig-Fehler (Beispiel)
exit(2);
} Cronjobs kapsle ich mit Lockfiles oder verteilten Locks, damit keine Parallelstarts zu Lastspitzen und Fehlersalven führen. Ich plane Retry-Fenster so, dass sie nicht mit Peak-Traffic kollidieren. Auch hier gilt: kontextreiche Logs schlagen jeden Stummel-Stacktrace.
Datenschutz, Aufbewahrung und Maskierung vertiefen
Über das reine “nicht loggen” hinaus implementiere ich Maskierungsregeln: Tokens und Passwörter ersetze ich durch Platzhalter, IPs speichere ich gekürzt, personenbezogene IDs pseudonymisiere ich (Hash mit Salt). Ich lege für jede Umgebung klare Retention-Zeiten fest und lösche Altbestände automatisiert. Exportpfade (z. B. Support-Bundles) sind zusätzlich verschlüsselt und rollenbasiert zugreifbar.
Exceptions prüfe ich auf sensible Inhalte (SQL mit Klarwerten, interne Hostnamen). Ich erziehe Teams dazu, hilfreiche, aber neutrale Fehltexte zu formulieren. Datenschutz beginnt im Code – der Logger ist nur die letzte Instanz, nicht der erste Filter.
Versionen, Deprecations und Migrationsfenster
Bei PHP-Upgrades beschreibe ich ein Migrationsfenster: In STAGE werte ich E_DEPRECATED streng aus, in PROD logge ich sie sichtbar, aber ohne Alarmierung. Ich unterscheide Deprecations aus meiner Codebasis und aus Drittpaketen und plane Fixes iterativ. Ein dedizierter Testfall stellt sicher, dass Deprecations nicht die UI verschmutzen und ausschließlich in Logs landen.
Ich halte zudem eine Kompatibilitätsmatrix zu Extensions bereit. Wenn Komponenten temporär divergieren, drossele ich Log-Lautstärke gezielt, ohne kritische Klassen zu entschärfen. Ziel ist stets: sauber fixen, nicht verstecken.
SLOs, Error Budgets und Alarm-Feinsteuerung
Ich messe nicht nur absolute Fehlzahlen, sondern definiere Fehlerraten-SLOs pro Endpunkt. Aus dem Error Budget leite ich Deployment-Frequenz und Watch-Modus ab: Wird das Budget knapp, erhöhe ich Vorsicht, aktiviere Sampling strenger und priorisiere Qualitätsarbeit. Alarme dedupliziere ich zeitbasiert und cluster sie nach Ursache (gleicher Stacktrace, gleicher Endpunkt), damit On-Call handlungsfähig bleibt.
Webserver-Fehlerseiten, FPM-Ausfälle und Caching-Fallen
Geht FPM in die Knie oder liefert 502/504, dient die statische 50x-Seite als verlässlicher Fallback. Diese Seite enthält weder Build-Infos noch interne Links, aber klare Hinweise für Nutzer und Support-Kontakte. Ich sorge dafür, dass CDNs und Reverse-Proxies 5xx nicht zwischenspeichern und Retry-After-Header respektieren. Bei Wartungsfenstern sende ich 503 mit Retry-After, nicht 500, und halte Maintenance-Pages außerhalb von PHP vor.
Für Requests mit JSON-Akzeptanz biete ich bei 5xx optional eine minimale JSON-Fehlerantwort aus dem Webserver an, damit Clients nicht ins Leere laufen. Gleichzeitig vermeide ich, dass der Webserver interne Pfade oder Module preisgibt – Sicherheit geht auch beim Fallback vor Komfort.
Praxisnahe Zusammenfassung
Ich trenne konsequent DEV und PROD, schalte Anzeigen in Live ab und logge vollständig. Custom-Handler liefern mir Kontrolle über Reaktion und Kontext. Ein klarer Error-Level, sinnvolle Filter und saubere Rotation reduzieren Rauschen. Security-Filter schützen Geheimnisse, während Alarme nur bei echten Problemen feuern. So bleibt die Oberfläche ruhig, die Logs sprechen Klartext und die server stability steigt spürbar.
Wer dieses Set-up befolgt, bewegt sich weg vom Feuerlöschen hin zu vorausschauendem Betrieb. Deployments werden kalkulierbar, Störungen kürzer und Analysen wiederholbar. Genau darum lohnt sich die Investition in eine saubere Konfiguration. Ich setze diese Prinzipien in jedem Projekt um und schlafe ruhiger. Produktion braucht keine Magie, sondern klare Regeln und disziplinierte Umsetzung.


