Viele Entwickler deployen auf gemanagte Cloud-Dienste und haben nie die volle Kontrolle über ihre Infrastruktur. Ich betreibe meinen Produktionsserver selbst — ein Hetzner Dedicated Server mit openSUSE Leap. Hier teile ich das komplette Setup.
Hardware und Betriebssystem
Der Server steht im Hetzner-Rechenzentrum in Falkenstein:
- CPU: Intel Core i7-6700 (4 Cores / 8 Threads, 3.4 GHz)
- RAM: 64 GB DDR4
- Storage: 2x 512 GB NVMe SSD im Software-RAID
- OS: openSUSE Leap 15.6
Warum openSUSE? Stabile Enterprise-Basis (SUSE Linux Enterprise), lange Support-Zyklen, exzellenter Paketmanager (zypper), und eine saubere Trennung zwischen System- und Drittanbieter-Paketen.
nginx mit Brotli: Maximale Komprimierung
nginx ist als Reverse Proxy und Webserver konfiguriert — mit Brotli-Modul für bessere Komprimierung als gzip:
server {
listen 443 ssl http2;
server_name www.example.de;
root /srv/www/vhosts/example.de/httpdocs/public;
# Dual compression: Brotli bevorzugt, gzip als Fallback
gzip on;
gzip_vary on;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript;
brotli on;
brotli_static on; # Serviert pre-komprimierte .br Dateien
brotli_comp_level 6;
brotli_types text/plain text/css application/json application/javascript;
location / {
try_files $uri /index.php$is_args$args;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php-fpm/www.sock;
}
}
brotli_static on ist der Schlüssel: Unsere Build-Pipeline erzeugt bereits .br-Dateien neben den Originalen. nginx serviert diese direkt, ohne zur Laufzeit komprimieren zu müssen. Brotli liefert 20-30% kleinere Dateien als gzip bei vergleichbarer CPU-Last. Implementiert habe ich Brotli in erster Linie aus SEO-Gründen: Neben gzip auch Brotli-Komprimierung anzubieten verbessert die Ladezeiten und wird von Suchmaschinen wie Google als positives Signal für das Ranking gewertet.
PHP-FPM via Unix Socket
PHP-FPM kommuniziert über einen Unix Socket statt TCP — das ist schneller, weil der gesamte TCP/IP-Stack umgangen wird:
upstream php-handler {
server unix:/run/php-fpm/www.sock;
}
Der PHP-FPM-Pool läuft unter dem nginx-User, damit es keine Dateirechte-Konflikte gibt.
Security Headers
Jede Response enthält Security-Headers nach aktuellen Best Practices:
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;" always;
add_header Referrer-Policy "no-referrer" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "0" always; # XSS-Auditor wurde aus allen Browsern entfernt, CSP ist der korrekte Schutz
fastcgi_hide_header X-Powered-By;
Catch-All: Kein Fingerprinting
Requests an unbekannte Hostnamen werden sofort verworfen — ohne Antwort, ohne Zertifikats-Preisgabe:
# HTTP: Verbindung sofort schließen
server {
listen 80 default_server;
server_name _;
return 444;
}
# HTTPS: TLS-Handshake abbrechen
server {
listen 443 ssl default_server;
server_name _;
ssl_reject_handshake on;
}
Das verhindert, dass Port-Scanner die gehosteten Domains ermitteln können.
Mehrschichtige Sicherheitsarchitektur
SSH-Tarpit mit endlessh-go
Port 22 ist absichtlich offen — aber dahinter läuft kein SSH-Server, sondern endlessh-go. Dieses Tool hält SSH-Scanner endlos in einer Fake-Session gefangen, ohne echte Ressourcen zu verbrauchen. Der echte SSH-Server läuft auf einem anderen Port mit Key-Only-Authentifizierung.
CrowdSec als fail2ban-Nachfolger
CrowdSec analysiert Logs und blockiert Angreifer — ähnlich wie fail2ban, aber mit Community-Intelligence: Wenn ein Angreifer bei einem anderen CrowdSec-Nutzer auffällt, wird seine IP automatisch auch bei mir geblockt.
# CrowdSec installieren
sudo zypper install crowdsec crowdsec-firewall-bouncer-nftables
# Status prüfen
sudo cscli metrics
sudo cscli alerts list
Firewall mit nftables
firewalld mit nftables-Backend als Whitelist-Ansatz: Nur explizit erlaubte Ports sind von außen erreichbar (HTTP, HTTPS, Mail). Interne Dienste wie RabbitMQ sind nur auf localhost gebunden.
Let's Encrypt Wildcard-Zertifikate
Ein einziges Wildcard-Zertifikat für alle Domains — über DNS-Challenge, nicht HTTP-Challenge:
# certbot mit DNS-Plugin für INWX
certbot certonly \
--dns-inwx \
--dns-inwx-credentials /root/.inwx-credentials \
-d "*.wunner-software.de" \
-d "wunner-software.de"
Der Vorteil: Kein Port-80-Zugriff nötig, funktioniert auch für interne Dienste. Ein Deploy-Hook verteilt das erneuerte Zertifikat automatisch an nginx, Mailcow und RabbitMQ.
Automatisierte Backups
Ein PHP-Script mit pcntl_fork() für parallele Ausführung sichert täglich um 04:00 Uhr:
- Alte Backups löschen
- Parallel: nginx-Config, PHP-Config, Repositories, vHosts, Datenbanken
- Mailcow-Backup
- Nextcloud in Maintenance-Mode → tar + mysqldump → Maintenance-Mode aus
- rsync zur Hetzner Storage Box (nur wenn alle Tasks erfolgreich)
# Dry-Run zum Testen
php /root/scripts/backup-all.php --dry-run
Docker für isolierte Dienste
Nicht alles läuft nativ. Einige Dienste laufen isoliert in Docker:
- Mailcow: Kompletter Mailserver (Postfix, Dovecot, SOGo, rspamd)
- Umami: Datenschutzfreundliche Web-Analytics
- Remark42: Selbst gehostetes Blog-Kommentarsystem
- endlessh-go: SSH-Tarpit
Diese Dienste sind über nginx als Reverse Proxy erreichbar, gebunden an 127.0.0.1.
Sauberes Start/Stop mit systemd
Die Container-eigene Restart-Policy (unless-stopped) fängt Crashes ab, aber beim Server-Shutdown werden Container einfach gekillt — kein docker compose down, keine saubere Reihenfolge. Deshalb hat jedes Docker-Compose-Projekt einen eigenen systemd-Service:
[Unit]
Description=Mailcow Docker Compose
Requires=docker.service
After=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/root/mailcow-dockerized
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=300
TimeoutStopSec=120
[Install]
WantedBy=multi-user.target
Type=oneshot mit RemainAfterExit=yes ist der Schlüssel: systemd betrachtet den Service als aktiv, auch nachdem docker compose up -d sich beendet hat. Beim Shutdown ruft systemd dann ExecStop auf — und die Container werden sauber heruntergefahren.
Über After= und Requires= lässt sich die Startreihenfolge steuern. Mailcow startet zuerst, da die anderen Projekte auf dessen Docker-Netzwerk oder Dienste angewiesen sein können:
Start: docker → Mailcow → endlessh + Umami + Remark42 (parallel) Stop: endlessh + Umami + Remark42 (parallel) → Mailcow → docker
Monitoring mit Grafana Cloud
Grafana Alloy (ehemals Grafana Agent) sammelt Metriken und Logs und sendet sie an Grafana Cloud Free. Damit habe ich Dashboards für CPU, RAM, Disk, nginx-Requests und CrowdSec-Alerts — ohne einen eigenen Grafana-Server betreiben zu müssen.
Fazit
Ein selbst verwalteter Server bedeutet mehr Arbeit als ein PaaS — aber auch volle Kontrolle, bessere Performance und niedrigere Kosten. Meine Motivation für dieses Setup war einfach: Ich kenne die Konsolenbefehle bereits und spare mir damit die Kosten für Tools wie Parallels & Co. Die initiale Einrichtung dauert, doch mit systemd, automatisierten Backups und CrowdSec läuft das System danach weitgehend wartungsfrei. Und ganz ehrlich — Linux ist heutzutage nicht mehr schwer zu bedienen, die Zeiten kryptischer Konfigurationen sind längst vorbei. Mit openSUSE Leap hat man zudem eine Distribution, die binärkompatibel mit dem Enterprise-Derivat SUSE Linux Enterprise ist, was langfristige Stabilität und professionellen Support garantiert. Und das Wissen, das man dabei aufbaut, ist in jedem DevOps-Kontext wertvoll.
Kommentare
Kommentare werden von Remark42 bereitgestellt. Beim Laden werden Daten an unseren Kommentar-Server übertragen.