
Server-Setup & Docker in Produktion

Die Baseline-Härtung des Ubuntu LTS vServers
Wir haben in den letzten 14 Teilen unserer Serie eine hochperformante, SEO-optimierte Headless-Architektur mit Contao und Next.js aufgebaut. Lokal (auf localhost) läuft alles perfekt. Doch jetzt wird es ernst: Wir bringen das System in das öffentliche Netz.
Die schlüsselfertige Bereitstellung und der dauerhafte Betrieb moderner Web-Applikationen auf einem Linux-vServer erfordern ein systematisches Härtungs- und Optimierungskonzept. Ein ungeschützt an das Internet angebundener Ubuntu-Server gerät bereits wenige Minuten nach der Initialisierung in das Visier automatisierter Botnets und Port-Scanner.
Die erste Regel für den produktiven Betrieb lautet daher: Die Sicherheit (Hardening) muss am ersten Tag (Day One) erfolgen, nicht erst nach einem Vorfall.
Die ersten Schritte nach dem Provisioning
Der Lebenszyklus eines produktionsbereiten vServers beginnt mit der Herstellung eines definierten, aktualisierten Softwarestands. Da Distributionen ab Werk häufig veraltete Paketstände mit bekannten Sicherheitslücken ausliefern, müssen der Paketindex aufgefrischt und alle ausstehenden Sicherheitsupdates sofort eingespielt werden.
# 1. System vollständig aktualisieren
sudo apt update && sudo apt upgrade -yWenn bei diesem Prozess ein neuer Kernel installiert wird, ist ein kontrollierter Systemneustart zwingend erforderlich, um den gehärteten Kernel-Code in den Arbeitsspeicher zu laden.
Um zu verhindern, dass du diese kritischen Updates im Alltag vergisst (z. B. durch Feature-Arbeit oder "On-Call Fatigue"), ist die Automatisierung unerlässlich. Manuelles Patchen scheitert in der Praxis fast immer. Wir richten daher "Unattended Upgrades" ein, damit Ubuntu kritische Sicherheitslücken vollautomatisch im Hintergrund schließt.
# 2. Automatische Sicherheitsupdates installieren
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgradesSSH-Zugang härten: Die Haustür verschließen
SSH (Port 22) ist das primäre Ziel für automatisierte Brute-Force-Angriffe. Ubuntu LTS wird zwar mit vernünftigen Standardwerten ausgeliefert, aber "vernünftig" ist im Internet nicht "gehärtet". Die Sicherung des SSH-Zugriffs ist der Schritt mit dem höchsten Return on Investment in der ersten Stunde nach der Server-Bereitstellung.
Wir deaktivieren die Passwort-Authentifizierung komplett und verlassen uns ausschließlich auf kryptografische Schlüssel (Key-Based Authentication, idealerweise ed25519). Zudem verbieten wir dem Benutzer root den direkten Login über SSH.
1# 3. SSH-Konfiguration anpassen (/etc/ssh/sshd_config)
2sudo nano /etc/ssh/sshd_config
3
4# Stelle sicher, dass folgende Werte gesetzt sind:
5PermitRootLogin no
6PasswordAuthentication no
7
8# 4. SSH-Dienst neu laden (Testen den Login vorher zwingend in einem zweiten Tab!)
9sudo systemctl reload sshZusätzlich installieren und aktivieren wir Fail2Ban. Dieses Tool überwacht die Authentifizierungs-Logs (auth.log) und blockiert IP-Adressen (über die Firewall) temporär, die durch zu viele fehlerhafte Login-Versuche auffallen. Für Produktionsserver ist dies ein extrem leichtgewichtiges und effektives Mittel gegen Brute-Force-Botnetze.
# 5. Fail2Ban installieren und aktivieren
sudo apt install fail2ban -y
sudo systemctl enable --now fail2banMit dieser Baseline-Härtung (Updates, automatische Patches, Key-Only-SSH und Fail2Ban) haben wir die wichtigsten Angriffsvektoren auf Betriebssystemebene bereits massiv entschärft.

Docker & UFW – Das unsichtbare Firewall-Leck
Nachdem unser vServer gehärtet ist, installieren wir die Docker-Engine. Doch hier wartet eine der gefährlichsten und gleichzeitig am häufigsten übersehenen Sicherheitslücken auf Administratoren: Docker umgeht die UFW-Firewall (Uncomplicated Firewall) von Ubuntu standardmäßig komplett.
Warum deine Firewall bei Docker wirkungslos ist
Die UFW ist eigentlich nur ein benutzerfreundliches Frontend für die Linux-interne Firewall iptables. Das Problem: Docker manipuliert iptables ebenfalls direkt, um das Netzwerk-Routing für Container zu verwalten, und klinkt sich dabei in der Regel vor den UFW-Regeln ein.
Was bedeutet das in der Praxis? Angenommen, du aktivierst UFW und erlaubst strikt nur Port 80 (HTTP) und 443 (HTTPS). Danach startest du einen Docker-Container (z. B. für eine Test-Datenbank oder ein Backend-Tool) mit dem Port-Mapping -p 8080:80. Du fühlst dich sicher, da UFW den Port 8080 ja standardmäßig blockiert. Die Realität: Dein Port 8080 ist nun für das gesamte Internet offen. Docker hat die UFW-Regeln einfach überschrieben. Dies ist kein Bug, sondern eine bewusste Designentscheidung von Docker.
Lösungsansatz 1: Binding an Localhost (Der sicherste Weg)
Für Dienste, die niemals direkt aus dem Internet erreichbar sein sollen (wie unsere MySQL-Datenbank oder Redis), ist der einfachste und sicherste Weg, den Container-Port ausschließlich an das lokale Interface (127.0.0.1) des Hosts zu binden. Ein Reverse-Proxy (wie Nginx oder Traefik) kann dann als einziger Container Ports nach außen öffnen (0.0.0.0:80 und 0.0.0.0:443) und den Traffic intern weiterleiten.
In deiner docker-compose.yml sieht das so aus:
1version: '3.8'
2services:
3 mysql:
4 image: mariadb:10.11
5 ports:
6 # SICHER: Die Datenbank ist nur vom Host-System selbst erreichbar!
7 # UFW-Bypass ist hier irrelevant.
8 - "127.0.0.1:3306:3306"
9
10 nginx:
11 image: nginx:alpine
12 ports:
13 # GEWOLLT: Nginx ist als einziger Dienst weltweit erreichbar.
14 - "0.0.0.0:80:80"
15 - "0.0.0.0:443:443"Lösungsansatz 2: Die DOCKER-USER Chain mit ufw-docker kontrollieren
Wenn du Container hast, die externe Ports benötigen, aber dennoch von UFW-Regeln (wie IP-Whitelisting) profitieren sollen, müssen wir die sogenannte DOCKER-USER Chain in iptables anpassen.
Da die manuelle Konfiguration von iptables komplex und fehleranfällig ist, hat sich in der Community das Open-Source-Tool ufw-docker als De-facto-Standard etabliert. Es fügt die notwendigen Sicherheitsregeln in die /etc/ufw/after.rules Datei ein, sodass Docker-Traffic standardmäßig blockiert wird und du ihn explizit über UFW freigeben musst.
So integrierst du das Tool auf deinem Ubuntu-Server:
1# 1. Das ufw-docker Skript herunterladen
2sudo wget -O /usr/local/bin/ufw-docker \
3 https://github.com/chaifeng/ufw-docker/raw/master/ufw-docker
4
5# 2. Ausführrechte setzen
6sudo chmod +x /usr/local/bin/ufw-docker
7
8# 3. Das Skript installieren (schreibt die Regeln in /etc/ufw/after.rules)
9sudo ufw-docker install
10
11# 4. UFW neustarten, um die Änderungen anzuwenden
12sudo systemctl restart ufwNach der Installation ist die Firewall wieder dicht. Wenn du nun einen Container für eine bestimmte externe IP-Adresse freigeben möchtest, nutzt du einfach das Tool:
# Beispiel: Port 8000 des Containers "my-app" nur für eine spezifische IP freigeben
sudo ufw-docker allow my-app 8000/tcp 192.168.1.100Mit diesen beiden Lösungsansätzen hast du das tückischste Sicherheitsproblem beim Betrieb von Docker auf einem vServer erfolgreich eliminiert.

Die Docker-Daemon Konfiguration – Log-Limits und Live Restore
Docker ist auf Ubuntu schnell mit einem simplen apt install eingerichtet. Für die lokale Entwicklung reicht das völlig aus. Wenn du diese Standardkonfiguration jedoch auf einem Produktionsserver belässt, steuerst du unweigerlich auf zwei katastrophale Ausfallszenarien zu: Volllaufende Festplatten und unnötige Downtimes bei System-Updates.
Um das zu verhindern, müssen wir das Herzstück von Docker – den Docker-Daemon – über die zentrale Steuerdatei /etc/docker/daemon.json explizit für den Live-Betrieb konfigurieren.
Gefahr 1: Unendliche Log-Dateien
Standardmäßig speichert Docker die Konsolenausgaben (Logs) deiner Container in Textdateien. Es gibt dabei kein Limit. Wenn dein Next.js-Frontend oder deine Laravel-Queue über Wochen hinweg Traffic verarbeiten oder Fehler loggen, wachsen diese Dateien ungebremst an, bis die Festplatte des vServers zu 100 % belegt ist. Die Folge: Der gesamte Server friert ein, Datenbanken korrumpieren und nichts geht mehr.
Wir beheben das, indem wir den log-driver hart limitieren.
Gefahr 2: Downtime beim Docker-Update
Das zweite Problem betrifft die Hochverfügbarkeit. Wenn der Docker-Daemon (der Hintergrunddienst) gestoppt oder aktualisiert wird, beendet er standardmäßig rigoros alle laufenden Container. Ein simples Sicherheitsupdate der Docker-Engine durch Ubuntu bedeutet also eine harte Downtime für deine Webseiten.
Die Lösung hierfür heißt Live Restore. Wenn diese Option aktiviert ist, entkoppelt sich der Docker-Daemon vor dem Herunterfahren von den laufenden Containern. Die Container laufen unter der Kontrolle der untergeordneten Laufzeitumgebung (containerd) autark weiter. Startet der Daemon nach dem Update wieder, verbindet er sich nahtlos mit den laufenden Instanzen, ohne dass deine Nutzer eine Unterbrechung bemerken.
Praxis & Code: Die produktive daemon.json
Wir legen nun die Konfigurationsdatei an, um Log-Limits zu setzen, Live Restore zu aktivieren und zusätzliche Sicherheitsrichtlinien durchzusetzen.
# 1. Die Konfigurationsdatei öffnen oder erstellen
sudo nano /etc/docker/daemon.jsonFüge dort das folgende JSON-Konstrukt ein:
1{
2 "log-driver": "json-file",
3 "log-opts": {
4 "max-size": "10m",
5 "max-file": "3"
6 },
7 "live-restore": true,
8 "userland-proxy": false,
9 "no-new-privileges": true,
10 "icc": false
11}Was macht diese Konfiguration im Detail?
max-size: "10m"undmax-file: "3": Docker rotiert die Logs. Kein Container wird jemals mehr als 30 Megabyte an Log-Daten ansammeln.live-restore: true: Erlaubt Zero-Downtime-Upgrades der Docker-Engine.userland-proxy: false: Schaltet den ressourcenfressenden Userspace-Proxy ab. Docker verlässt sich nun rein auf schnelleiptables-NAT-Regeln für das Port-Routing.no-new-privileges: trueundicc: false: Verhindert, dass Prozesse innerhalb von Containern zusätzliche Rechte erlangen, und unterbindet die unkontrollierte Kommunikation aller Container untereinander im Standard-Netzwerk (Inter-Container Communication).
Um diese lebenswichtigen Änderungen zu aktivieren, müssen wir den Docker-Dienst neu laden. Da live-restore bisher noch nicht aktiv war, wird dieser Neustart die Container einmalig beenden. Bei allen zukünftigen Updates greift das neue Netz jedoch.
# 2. Die Konfiguration anwenden
sudo systemctl restart docker
MySQL 8 Performance-Tuning im Container
Wir haben den Server gehärtet und die Docker-Engine für den ausfallsicheren Betrieb konfiguriert. Jetzt widmen wir uns dem Motor unserer Headless-Architektur: Der MySQL- (oder MariaDB-) Datenbank.
Wenn du das offizielle MySQL-Docker-Image startest, läuft die Datenbank mit einer Standardkonfiguration, die für kleinste Testumgebungen gedacht ist. Im produktiven Headless-Betrieb, wo Next.js bei jedem Build oder Cache-Revalidation tausende Queries auf die Contao-API abfeuert, bricht diese Standard-Datenbank unter Last sofort zusammen.
Die Gesamtleistung einer MySQL-8-Datenbank steht in direktem Zusammenhang mit der Effizienz der Arbeitsspeicher-Nutzung durch die InnoDB Storage Engine.
Die OOM-Killer Falle bei Docker-Datenbanken
Die zentrale Stellschraube für die Performance ist der innodb_buffer_pool_size. Dieser Puffer hält häufig abgefragte Tabellendaten und Indizes im RAM, um extrem langsame Festplattenzugriffe (Disk I/O) zu minimieren.
Auf dedizierten Datenbank-Servern werden üblicherweise 60 % bis 80 % des verfügbaren Hauptspeichers für den Buffer Pool reserviert. Doch Vorsicht: Wenn MySQL innerhalb eines Docker-Containers läuft, muss die Konfiguration strikt an die Limits des Containers (bzw. des vServers) angepasst werden. Übersteigt der Speicherbedarf des MySQL-Prozesses – also der Buffer Pool zuzüglich Thread-Speicher wie join_buffer_size – das zugewiesene RAM-Limit, wird der Container unweigerlich vom "OOM-Killer" (Out of Memory) des Linux-Kernels gnadenlos beendet.
Praxis & Code: Die perfekte my.cnf für Headless-Lasten
Um Flaschenhälse zu vermeiden, mounten wir eine eigene Optimierungsdatei in unseren Datenbank-Container. Lege dazu eine Datei namens 90_optimize.cnf an und binde sie in deiner docker-compose.yml unter /etc/mysql/conf.d/ ein.
Hier ist eine bewährte Basiskonfiguration für einen vServer mit 8 GB RAM, bei dem ca. 4 GB exklusiv für die Datenbank reserviert werden können:
Ini, TOML
1[mysqld]
2# 1. Der Buffer Pool: Das Herzstück der Performance (~50-70% des DB-RAMs)
3# Minimiert Disk-I/O drastisch durch In-Memory-Caching
4innodb_buffer_pool_size = 3G
5innodb_buffer_pool_instances = 3
6
7# 2. Schreib-Performance (IOPS) optimieren
8# Verhindert Eil-Flushing bei Massen-Schreiboperationen (Contao Cache-Warmups)
9innodb_redo_log_capacity = 1G
10# Wert '2' bedeutet: Reduktion von Schreiblatenzen durch OS-Page-Caching (ideal für Web-Workloads)
11innodb_flush_log_at_trx_commit = 2
12innodb_flush_method = O_DIRECT
13
14# 3. Verbindungen und Thread-Management
15# Begrenzung simultaner Threads zur Verhinderung von Speichererschöpfung (OOM)
16max_connections = 200
17thread_cache_size = 50
18
19# 4. Tabellen und Caches
20# Verhindert wiederholtes Parsen von Tabellendefinitionen im laufenden Betrieb
21table_definition_cache = 4000
22table_open_cache = 4000
23
24# Speichergrenzen für temporäre Tabellen im RAM anheben
25tmp_table_size = 64M
26max_heap_table_size = 64MTipp für Contao: Der Wert innodb_flush_log_at_trx_commit = 2 ist ein absoluter Gamechanger für schreibintensive CMS-Operationen. Er verzögert den Flush auf die Festplatte um maximal eine Sekunde, was die Performance beim Speichern im Backend oder beim Generieren von Caches massiv erhöht, bei einem vertretbaren (1 Sekunde) Datenverlust-Risiko im Falle eines harten Server-Crashs.
Erfolgskontrolle mit MySQLTuner
Du musst nicht raten, ob deine Parameter passen. Sobald dein System einige Tage unter echter Last läuft, kannst du das Skript MySQLTuner nutzen. Da wir den Host-Server sauber halten wollen, führen wir den Tuner einfach als temporären Docker-Container aus, der sich mit unserer laufenden Datenbank verbindet:
docker run --rm --net=host \
jmrenouard/mysqltuner:latest \
--host 127.0.0.1 \
--user root \
--pass DeinSicheresPasswortDas Tool bewertet die Puffer-Trefferquote, prüft die Auslastung des Thread-Caches und identifiziert temporäre Tabellen, die aufgrund unzureichender RAM-Limits auf die langsame Festplatte ausgelagert werden mussten. Anhand der roten Warnungen im Output kannst du deine 90_optimize.cnf im Laufe der Zeit perfekt auf dein Projekt abstimmen.

Fazit – Deine Festung im Netz
Mit dem Abschluss von Teil 15 haben wir den entscheidenden Schritt aus der sicheren lokalen Entwicklungsumgebung in die raue Realität des öffentlichen Internets gemacht. Ein Headless CMS wie Contao, gepaart mit einem rasend schnellen Next.js-Frontend, kann seine Stärken nur dann voll ausspielen, wenn das Fundament unerschütterlich ist.
Wir haben die kritischsten Schwachstellen systematisch eliminiert:
Die Ubuntu-Baseline: Durch automatische Sicherheitsupdates, Key-Only-SSH und Fail2Ban haben wir die "Haustür" für Botnetze und Skript-Kiddies fest verriegelt.
Das Firewall-Leck gestopft: Mit der bewussten Konfiguration der
DOCKER-USERChain überufw-docker(oder durch striktes Localhost-Binding) haben wir verhindert, dass Docker deine UFW-Regeln heimlich umgeht und sensible Ports freilegt.Zero-Downtime & Stabilität: Durch
live-restorein derdaemon.jsonüberleben unsere Container künftige Docker-Updates nahtlos. Limitierte JSON-Logs verhindern volllaufende Festplatten.MySQL High-Performance: Durch das Tuning des
innodb_buffer_pool_sizeund der Caching-Parameter in der90_optimize.cnfhaben wir den Datenbank-Container vor dem gefürchteten Linux-OOM-Killer bewahrt und für schreibintensive Headless-Lasten optimiert.
Während dein Server nun autonom und sicher arbeitet, hast du den Kopf wieder frei für die kreativen Aspekte deines Geschäfts – sei es die Konzeption neuer Features, die Optimierung deiner Workflows oder das nächste Immobilien-Shooting im Raum Sassnitz. Und falls doch einmal eine Warnmeldung aufleuchtet, kannst du dir in Ruhe einen frischen Kaffee aus deiner Krups-Maschine lassen: Du weißt jetzt exakt, an welchen Stellschrauben (Logs, UFW, OOM) du drehen musst.
Contao Headless Masterclass: High-Performance mit Next.js Pillar
Contao Headless Architektur: Das Konzept im Detail verstehen
Projektstruktur & Entwicklungsumgebung für Contao und Next.js
Contao Headless Basis-Setup: Installation & Bildkonfiguration
Contao Headless Datenstruktur & Redakteurs-Erlebnis
Die API From Scratch entwickeln: Symfony Routing in Contao 5
Contao Headless API-Sicherheit & Formularverarbeitung
Next.js Setup & Grundstruktur für dein Headless CMS
Dynamisches Routing & Navigation in Next.js
Module & Formulare im Frontend rendern
Der Draft- & Preview-Modus im Headless CMS
High-Performance & Caching-Strategien
SEO-Masterclass & Schema-Validierung
Semantisches SEO & Content-Struktur
Core Web Vitals & PageSpeed
Server-Setup & Docker in Produktion
Häufig gestellte Fragen (FAQ)
Weil Docker standardmäßig die UFW-Firewall ignoriert. Wenn du -p 3306:3306 in deiner docker-compose.yml definierst, greift Docker direkt auf iptables zu und öffnet den Port weltweit, selbst wenn UFW ihn blockiert. Durch das explizite Binding an Localhost (-p 127.0.0.1:3306:3306) wird der Port erst gar nicht auf dem externen Netzwerkinterface deines Servers lauschen.
Ja, das ist problemlos möglich. Du fügst den Eintrag in die /etc/docker/daemon.json ein. Beachte jedoch: Damit Docker diese neue Einstellung übernimmt, musst du den Daemon einmalig mit sudo systemctl restart docker neu starten. Bei diesem ersten Neustart werden die Container noch einmalig gestoppt. Bei allen zukünftigen Updates bleiben sie dann dank live-restore online.
Das sicherste Indiz liefert das Tool MySQLTuner. Wenn dort die "InnoDB Buffer Pool Read Efficiency" sinkt oder gewarnt wird, dass temporäre Tabellen auf die Festplatte geschrieben werden ("Temporary tables created on disk"), ist das RAM-Limit zu eng. Ein weiteres Alarmsignal ist der Befehl dmesg -T | grep -i oom. Wenn dort dein MySQL-Container auftaucht, hat der Server den Prozess wegen RAM-Mangel hart beendet.
Dein nächster Schritt: Phase 5 – aaPanel, Reverse Proxy & Backups
Wir haben nun einen kugelsicheren, nackten Linux-Server mit einer hochoptimierten Docker- und Datenbank-Instanz. Was uns jetzt noch fehlt, ist der Komfort einer modernen Steuerzentrale, die Auslieferung über sicheres HTTPS und ein narrensicheres Netz mit doppeltem Boden.
Im kommenden Teil 16: aaPanel, Reverse Proxy & Backups runden wir den produktiven Betrieb endgültig ab:
aaPanel als Kommandozentrale: Wir installieren ein leichtgewichtiges, modernes Server-Control-Panel, um Cronjobs, Verzeichnisse und Prozesse bequem visuell zu verwalten, ohne bei jeder Kleinigkeit in die SSH-Konsole wechseln zu müssen.
Reverse Proxy & Let's Encrypt: Wie wir Nginx (oder Traefik) als Reverse Proxy vor unsere Docker-Container schalten, um den Traffic zu routen und vollautomatisch kostenlose SSL-Zertifikate für alle unsere Domains zu generieren und zu erneuern.
Automatisierte Cloud-Backups: Die beste Hardware kann ausfallen. Wir richten automatisierte, tägliche Backup-Workflows (Datenbank-Dumps und Asset-Synchronisation) mit Tools wie
rcloneein, die unsere Daten sicher und verschlüsselt direkt in dein Google Drive spiegeln.
Jetzt starten: Teil 16 – aaPanel, Reverse Proxy & automatisierte Backups einrichten

Dietrich Bojko
Senior Webentwickler
Webinteger arbeitet seit vielen Jahren produktiv mit
Linux-basierten Entwicklungsumgebungen unter Windows.
Der Fokus liegt auf
performanten Setups mit WSL 2, Docker, PHP, Node.js und modernen
Build-Tools in realen Projekten –
nicht auf theoretischen Beispielkonfigurationen.
Die Artikel dieser Serie entstehen direkt aus dem täglichen Einsatz in Kunden- und Eigenprojekten und dokumentieren bewusst auch typische Fehler, Engpässe und bewährte Workarounds.


