Zum Inhalt springen
·13 Min. Lesezeit

Das VPS-Setup, das wirklich funktioniert: Node.js, PM2, Nginx und Zero-Downtime-Deploys

Das exakte VPS-Deployment-Setup, das ich in Produktion verwende — Ubuntu-Absicherung, PM2-Cluster-Modus, Nginx-Reverse-Proxy, SSL und ein Deploy-Script, das mich noch nie im Stich gelassen hat. Keine Theorie, nur das, was funktioniert.

Teilen:X / TwitterLinkedIn

Dieser Blog läuft auf einem 10$/Monat VPS. Nicht Vercel, nicht AWS, nicht ein Kubernetes-Cluster, der von einem sechsköpfigen Team verwaltet wird. Eine einzelne Ubuntu-Box mit Nginx, PM2 und einem Bash-Script, das in unter 30 Sekunden deployt.

Ich habe die anderen Wege probiert. Ich habe Vercel verwendet (großartig, bis man Cron-Jobs, persistente WebSockets oder einfach Kontrolle braucht). Ich habe AWS verwendet (großartig, wenn man gerne die halbe Zeit mit IAM-Policies verbringt). Am Ende lande ich immer wieder bei einem VPS.

Aber hier ist das Problem: Jedes "Deploy auf VPS"-Tutorial im Internet hört beim Happy Path auf. Sie zeigen dir, wie man Node.js installiert und node server.js ausführt und nennen es Produktion. Dann wird dein Server per SSH brute-forced, dein Prozess stirbt um 3 Uhr morgens, weil niemand einen Process Manager eingerichtet hat, und dein SSL-Zertifikat ist vor drei Monaten abgelaufen.

Dies ist die Anleitung, die ich mir gewünscht hätte. Alles hier ist kampferprobt — genau dieses Setup liefert die Seite aus, die du gerade liest.

Beginne mit Sicherheit, nicht mit Code#

Bevor du auch nur an Node.js denkst, sichere die Box ab. Frische VPS-Instanzen sind Ziele. Automatisierte Bots fangen innerhalb von Minuten nach der Bereitstellung an, deinen SSH-Port zu attackieren.

Erstelle einen Nicht-Root-Benutzer#

bash
adduser deploy
usermod -aG sudo deploy

Richte SSH-Key-Authentifizierung ein#

Auf deinem lokalen Rechner:

bash
ssh-copy-id deploy@your-server-ip

Dann deaktiviere die Passwort-Authentifizierung komplett:

bash
sudo nano /etc/ssh/sshd_config
bash
PasswordAuthentication no
PermitRootLogin no
bash
sudo systemctl restart sshd

Wenn du das überspringst, wirst du innerhalb von Tagen Tausende fehlgeschlagener Login-Versuche in deinen Auth-Logs sehen. Das ist keine Paranoia — das ist Dienstag im öffentlichen Internet.

Firewall mit UFW#

bash
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

Das war's. Vier Regeln. Nur SSH und Web-Traffic kommen durch.

Fail2Ban#

bash
sudo apt install fail2ban -y
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

Bearbeite /etc/fail2ban/jail.local:

ini
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600
bash
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

Drei fehlgeschlagene SSH-Versuche und du bist für eine Stunde gesperrt. Ich habe zugesehen, wie Fail2Ban an einem einzigen Tag Hunderte von IPs blockiert hat. Es funktioniert.

Unbeaufsichtigte Sicherheitsupdates#

bash
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure -plow unattended-upgrades

Dein Server wird jetzt automatisch Sicherheitspatches installieren. Eine Sache weniger, die man vergessen kann.

Node.js: Verwende NVM, nicht apt#

Das sehe ich in jedem Tutorial: sudo apt install nodejs. Tu das nicht.

Ubuntus Paketquellen liefern uralte Node.js-Versionen. Selbst das NodeSource-PPA hinkt hinterher. Und wenn du zwischen Node 20 und Node 22 für verschiedene Projekte wechseln musst, steckst du fest.

NVM löst das:

bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install --lts
nvm alias default lts/*

Jetzt überprüfen:

bash
node -v  # v22.x.x oder was auch immer LTS aktuell ist
npm -v

Der nicht offensichtliche Tipp: Wenn du globale Pakete mit NVM installierst (wie PM2), sind sie an diese Node-Version gebunden. Wenn du mit nvm use die Version wechselst, verschwinden deine Globals. Setze deinen Standard und bleibe auf dem Server dabei:

bash
nvm alias default 22

Das hat mich genau einmal erwischt. Einmal war genug.

PM2: Der Process Manager, der sich bezahlt macht#

PM2 ist der Unterschied zwischen "deployed" und "produktionsreif." Er übernimmt Prozessverwaltung, Clustering, Log-Rotation, Auto-Restart bei Abstürzen und Startup-Scripts. Kostenlos.

Installieren und einrichten#

bash
npm install -g pm2

Die Ecosystem-Konfiguration#

Starte Apps nicht mit CLI-Flags. Verwende eine ecosystem.config.js-Datei. Sie ist versionskontrolliert, reproduzierbar und selbstdokumentierend.

javascript
// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: "akousa",
      script: "node_modules/.bin/next",
      args: "start -p 3002",
      cwd: "/var/www/akousa.net",
      instances: 2,
      exec_mode: "cluster",
      max_memory_restart: "500M",
      env: {
        NODE_ENV: "production",
        PORT: 3002,
      },
      // Graceful Shutdown
      kill_timeout: 5000,
      listen_timeout: 10000,
      wait_ready: false,
      // Logging
      log_date_format: "YYYY-MM-DD HH:mm:ss Z",
      error_file: "/var/log/pm2/akousa-error.log",
      out_file: "/var/log/pm2/akousa-out.log",
      merge_logs: true,
      // Auto-Restart bei Fehler
      autorestart: true,
      max_restarts: 10,
      min_uptime: "10s",
      // Nicht in Produktion watchen
      watch: false,
    },
  ],
};

Lass mich die wichtigen Entscheidungen erklären:

instances: 2 statt "max": Auf einem kleinen VPS mit 1-2 Kernen klingt "max" clever, aber es werden Prozesse erzeugt, die während der Builds um Ressourcen kämpfen. Zwei Instanzen geben dir Zero-Downtime-Reloads und lassen trotzdem Spielraum. Auf einer 4+-Kern-Maschine, klar, verwende "max".

exec_mode: "cluster": Das ist, was Zero-Downtime-Reloads ermöglicht. Ohne Cluster-Modus ist pm2 reload nur ein verkleideter Restart. Mit Cluster-Modus startet PM2 Instanzen nacheinander neu — deine App geht nie komplett offline.

max_memory_restart: "500M": Deine Next.js-App hat ein Memory Leak? PM2 startet sie neu, bevor sie deinen Server per OOM-Kill erlegt. Das hat mich schon mehr als einmal vor 2-Uhr-morgens-Alarmen bewahrt.

kill_timeout: 5000: Gibt deiner App 5 Sekunden, um laufende Requests abzuschließen, bevor PM2 sie zwangsbeendet. Der Standardwert (1600ms) ist zu aggressiv für Apps mit Datenbankverbindungen.

watch: false: Ich habe Leute gesehen, die watch: true in Produktion gelassen haben. PM2 startet die App dann jedes Mal neu, wenn sich eine Log-Datei ändert. Deine App gerät in eine Restart-Schleife. Tu das nicht.

Startup-Script#

Lass PM2 Neustarts überleben:

bash
pm2 startup systemd
# Kopiere und führe den Befehl aus, den es ausgibt
pm2 save

Das generiert einen systemd-Service. Nach einem Server-Neustart kommt deine App automatisch zurück. Teste es — starte deinen Server neu und überprüfe es. Nimm es nicht einfach an.

Log-Rotation#

Logs werden irgendwann deine Festplatte auffressen. Installiere das Rotationsmodul:

bash
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress true

50MB Maximum pro Datei, 7 rotierte Dateien behalten, die alten komprimieren. Ohne das habe ich gesehen, wie /var/log eine 25GB-Festplatte innerhalb von drei Wochen bei einer mäßig frequentierten App gefüllt hat.

Nginx: Der Reverse Proxy, der mehr kann als du denkst#

"Warum nicht einfach Node.js direkt auf Port 80 freigeben?"

Weil Nginx Dinge übernimmt, für die Node.js keine Zyklen verschwenden sollte: SSL-Terminierung, statische Dateien ausliefern, Gzip-Komprimierung, Request-Buffering, Verbindungslimits und eleganten Umgang mit langsamen Clients. Es ist in C geschrieben und genau dafür gebaut.

Installieren#

bash
sudo apt install nginx -y

Die Konfiguration#

nginx
# /etc/nginx/sites-available/akousa.net
 
upstream node_app {
    server 127.0.0.1:3002;
    keepalive 64;
}
 
server {
    listen 80;
    listen [::]:80;
    server_name akousa.net www.akousa.net;
 
    # Alle HTTP-Anfragen auf HTTPS umleiten
    return 301 https://$host$request_uri;
}
 
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name akousa.net www.akousa.net;
 
    # SSL (von Certbot verwaltet — diese Zeilen werden automatisch hinzugefügt)
    ssl_certificate /etc/letsencrypt/live/akousa.net/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/akousa.net/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
 
    # Sicherheitsheader
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
 
    # Gzip-Komprimierung
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_min_length 256;
    gzip_types
        text/plain
        text/css
        text/javascript
        application/javascript
        application/json
        application/xml
        image/svg+xml
        application/wasm;
 
    # Proxy-Einstellungen
    location / {
        proxy_pass http://node_app;
        proxy_http_version 1.1;
 
        # Header
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
 
        # WebSocket-Unterstützung (falls benötigt)
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
 
        # Timeouts — großzügig aber nicht unendlich
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
 
        # Buffering — lass Nginx langsame Clients abfangen
        proxy_buffering on;
        proxy_buffer_size 16k;
        proxy_buffers 4 32k;
        proxy_busy_buffers_size 64k;
    }
 
    # Next.js statische Assets — lass Nginx sie direkt ausliefern
    location /_next/static/ {
        alias /var/www/akousa.net/.next/static/;
        expires 365d;
        access_log off;
        add_header Cache-Control "public, immutable";
    }
 
    # Öffentliche statische Dateien
    location /static/ {
        alias /var/www/akousa.net/public/static/;
        expires 30d;
        access_log off;
    }
 
    # Zugriff auf Punkt-Dateien blockieren
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }
}

Aktivieren:

bash
sudo ln -s /etc/nginx/sites-available/akousa.net /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

Führe immer nginx -t vor dem Neuladen aus. Ich habe einmal eine fehlerhafte Konfiguration gepusht und die Seite lahmgelegt, weil ich die Syntaxprüfung übersprungen habe. Die fünf Zeichen nginx -t hätten mir dreißig Minuten panisches Debugging erspart.

Dinge, die die meisten Tutorials in dieser Konfiguration übersehen:

upstream-Block mit keepalive 64: Nginx verwendet Verbindungen zu deinem Node.js-Backend wieder, anstatt für jeden Request eine neue TCP-Verbindung zu öffnen. Das ist unter Last relevant.

proxy_buffering on: Nginx liest die gesamte Antwort von Node.js in den Speicher und sendet sie dann mit der Geschwindigkeit an den Client, die der Client verarbeiten kann. Ohne das blockiert ein langsamer Client auf einer 3G-Verbindung deinen Node.js-Worker.

_next/static/ direkt ausliefern: Das sind gehashte, unveränderliche Assets. Lass Nginx sie von der Festplatte mit einem 365-Tage-Cache-Header ausliefern. Deine Node.js-Prozesse sollten keine Zeit damit verschwenden.

SSL in fünf Minuten#

Let's Encrypt hat SSL gelöst. Wenn du 2026 noch für Zertifikate bezahlst, hör auf damit.

bash
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d akousa.net -d www.akousa.net

Certbot fragt nach deiner E-Mail, akzeptiert die Nutzungsbedingungen und modifiziert automatisch deine Nginx-Konfiguration, um die SSL-Direktiven einzufügen. Das war's.

Automatische Verlängerung überprüfen#

Certbot installiert einen systemd-Timer, der zweimal täglich prüft und Zertifikate innerhalb von 30 Tagen vor Ablauf erneuert:

bash
sudo systemctl list-timers | grep certbot

Teste, ob die Verlängerung funktioniert:

bash
sudo certbot renew --dry-run

Wenn der Dry Run erfolgreich ist, wirst du nie wieder über SSL nachdenken müssen. Wenn er fehlschlägt, liegt es meist daran, dass Port 80 blockiert ist (prüfe deine UFW-Regeln) oder Nginx nicht läuft.

Eine Sache, die mich erwischt hat: Wenn du Nginx vor Certbot einrichtest, stelle sicher, dass dein Server-Block auf Port 80 lauscht, ohne die HTTPS-Umleitung. Certbot muss Port 80 für die HTTP-01-Challenge erreichen können. Nachdem Certbot erfolgreich gelaufen ist, dann füge die Umleitung hinzu.

Das Deploy-Script#

Dieses Script läuft jedes Mal, wenn ich in Produktion deploye. Keine CI/CD-Plattform, keine GitHub Actions. Nur SSH und Bash.

bash
#!/bin/bash
# deploy.sh — Deployment mit nahezu null Downtime
 
set -euo pipefail
 
APP_DIR="/var/www/akousa.net"
APP_NAME="akousa"
LOG_FILE="/var/log/deploy.log"
HEALTH_URL="http://localhost:3002"
MAX_RETRIES=10
RETRY_INTERVAL=3
 
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
 
log "=== Deploy gestartet ==="
 
cd "$APP_DIR"
 
# Neuesten Code ziehen
log "Ziehe neueste Änderungen..."
git pull origin main 2>&1 | tee -a "$LOG_FILE"
 
# Abhängigkeiten installieren
log "Installiere Abhängigkeiten..."
npm install --legacy-peer-deps 2>&1 | tee -a "$LOG_FILE"
 
# Bauen
log "Baue Anwendung..."
rm -rf .next
npm run build 2>&1 | tee -a "$LOG_FILE"
 
if [ $? -ne 0 ]; then
    log "FEHLER: Build fehlgeschlagen. Deploy wird abgebrochen."
    exit 1
fi
 
# PM2 neu laden (Zero-Downtime im Cluster-Modus)
log "Lade PM2 neu..."
pm2 reload "$APP_NAME" 2>&1 | tee -a "$LOG_FILE"
pm2 save 2>&1 | tee -a "$LOG_FILE"
 
# Health Check mit Wiederholungsversuchen
log "Führe Health Check aus..."
for i in $(seq 1 $MAX_RETRIES); do
    HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' "$HEALTH_URL" 2>/dev/null || echo "000")
    if [ "$HTTP_CODE" = "200" ]; then
        log "Health Check bestanden (HTTP $HTTP_CODE)"
        log "=== Deploy erfolgreich abgeschlossen ==="
        exit 0
    fi
    log "Health Check Versuch $i/$MAX_RETRIES (HTTP $HTTP_CODE). Neuer Versuch in ${RETRY_INTERVAL}s..."
    sleep $RETRY_INTERVAL
done
 
log "FEHLER: Health Check nach $MAX_RETRIES Versuchen fehlgeschlagen"
log "Rollback auf vorherigen PM2-Zustand..."
pm2 restart "$APP_NAME" 2>&1 | tee -a "$LOG_FILE"
exit 1

Ausführbar machen:

bash
chmod +x deploy.sh

Von deinem lokalen Rechner deployen:

bash
ssh root@your-server-ip "bash /var/www/akousa.net/deploy.sh"

Zentrale Entscheidungen in diesem Script:

set -euo pipefail: Das Script bricht bei jedem Fehler sofort ab. Ohne das läuft ein fehlgeschlagenes npm install still weiter in den Build-Schritt, und du bekommst einen kryptischen Fehler, dessen Debugging 20 Minuten kostet.

rm -rf .next vor dem Build: Next.js hat einen Build-Cache, der gelegentlich veraltete Ausgaben produziert. Das hat mich einmal erwischt — eine Seite zeigte alten Inhalt, obwohl der Quellcode aktualisiert war. Das Build-Verzeichnis zu löschen fügt vielleicht 15 Sekunden zum Build hinzu, garantiert aber frische Ausgabe.

pm2 reload statt pm2 restart: Das ist der Zero-Downtime-Teil. Im Cluster-Modus führt reload einen rollenden Neustart durch — es fährt neue Instanzen mit dem aktualisierten Code hoch, wartet bis sie bereit sind, und fährt dann die alten elegant herunter. Zu keinem Zeitpunkt laufen null Instanzen.

Health Check mit Wiederholungsversuchen: Next.js braucht ein paar Sekunden zum Aufwärmen nach dem Neustart. Das Script wartet bis zu 30 Sekunden (10 Versuche x 3 Sekunden) und prüft, ob die App mit HTTP 200 antwortet. Wenn nicht, stimmt etwas nicht, und du musst es sofort wissen — nicht erst durch einen Nutzer erfahren.

Rollback bei Fehler: Wenn der Health Check nach allen Versuchen fehlschlägt, startet das Script PM2 neu (was den zuletzt gespeicherten Zustand lädt). Es ist kein perfektes Rollback, aber besser als den Server in einem kaputten Zustand zu lassen.

Wenn um 2 Uhr morgens etwas kaputtgeht#

Hier ist, was ich bei genau diesem Setup tatsächlich debuggt habe:

"Die Seite ist down"#

Erste Befehle:

bash
pm2 status
pm2 logs akousa --lines 50
sudo systemctl status nginx
sudo tail -50 /var/log/nginx/error.log

Neun von zehn Mal sagt dir pm2 logs sofort, was passiert ist. Eine fehlende Umgebungsvariable, eine fehlgeschlagene Datenbankverbindung oder eine unbehandelte Promise-Rejection.

"Speicher wächst ständig"#

bash
pm2 monit

Das gibt dir ein Live-Dashboard von CPU und Speicher pro Prozess. Wenn der Speicher stetig steigt, ohne sich einzupendeln, hast du ein Leak. Die max_memory_restart-Einstellung in deiner Ecosystem-Konfiguration ist dein Sicherheitsnetz — PM2 startet den Prozess neu, bevor er den Server per OOM-Kill erlegt.

Für tiefere Untersuchung:

bash
pm2 describe akousa

Das zeigt Uptime, Restart-Zähler und Speicher-Snapshots. Wenn du 47 Restarts in den letzten 24 Stunden siehst, ist das dein Hinweis.

"SSL-Zertifikat abgelaufen"#

bash
sudo certbot certificates

Listet alle Zertifikate mit ihrem Ablaufdatum auf. Wenn die automatische Verlängerung fehlgeschlagen ist:

bash
sudo certbot renew --force-renewal
sudo systemctl reload nginx

"Festplatte voll"#

bash
df -h
du -sh /var/log/*
pm2 flush

pm2 flush löscht sofort alle PM2-Logdateien. Wenn du die Log-Rotation nicht eingerichtet hast (ich hab's dir gesagt), ist das der Moment, wo du den Schmerz spürst.

Der Befehl, den ich jeden Morgen ausführe#

bash
ssh deploy@akousa.net "pm2 status && df -h / && uptime"

Drei Dinge in einer Zeile: Laufen meine Prozesse, ist meine Festplatte in Ordnung, ist der Server überlastet. Dauert zwei Sekunden. Fängt Probleme auf, bevor es die Nutzer tun.

Was die meisten Anleitungen dir nicht sagen#

Dein Build-Schritt ist deine größte Schwachstelle. Auf einem VPS mit 1GB RAM kann npm run build für eine Next.js-App 800MB+ Speicher verbrauchen. Wenn PM2 deine App während des Builds in zwei Instanzen ausführt, bekommst du einen OOM. Lösungen: Verwende eine Swap-Datei (mindestens 2GB), oder stoppe die App während der Builds und akzeptiere ein paar Sekunden Downtime. Ich verwende Swap.

bash
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

--legacy-peer-deps in deinem Install-Befehl ist ein Code-Smell, keine Lösung. Ich verwende es, weil einige Pakete in meinem Abhängigkeitsbaum ihre Peer-Dependency-Bereiche nicht aktualisiert haben. Alle paar Monate versuche ich, es zu entfernen. Eines Tages wird es klappen. Bis dahin liefere ich aus.

Teste dein Deploy-Script von Grund auf. Klone dein Repository auf einem frischen Server und führe jeden Schritt manuell aus. Die Anzahl der "Works on my machine"-Probleme, die sich in Deploy-Scripts verstecken, ist peinlich. Ich habe drei Probleme in meinem gefunden, als ich das getan habe — fehlende globale Pakete, falsche Dateiberechtigungen und ein Pfad, der nur existierte, weil ich vorher etwas manuell eingerichtet hatte.

Trage die IP deines Servers in deine SSH-Konfiguration ein. Hör auf, IP-Adressen einzutippen:

bash
# ~/.ssh/config
Host akousa
    HostName 69.62.66.94
    User deploy
    IdentityFile ~/.ssh/id_ed25519

Jetzt ist ssh akousa alles, was du brauchst. Kleine Dinge summieren sich.

Die vollständige Checkliste#

Bevor du es als fertig bezeichnest:

  • Nicht-Root-Benutzer mit sudo-Zugang
  • Nur SSH-Key-Authentifizierung, Passwort-Auth deaktiviert
  • UFW aktiviert, nur notwendige Ports offen
  • Fail2Ban schützt SSH
  • Unbeaufsichtigte Sicherheitsupdates aktiviert
  • Node.js über NVM installiert
  • PM2 führt deine App im Cluster-Modus aus
  • PM2-Startup-Script konfiguriert (überlebt Neustarts)
  • PM2-Log-Rotation installiert
  • Nginx-Reverse-Proxy mit korrekten Headern
  • SSL über Let's Encrypt mit automatischer Verlängerung
  • Deploy-Script mit Health Checks
  • Swap-Datei konfiguriert (für Build-Spielraum)
  • Getestet: Server neustarten und überprüfen, ob alles wieder hochkommt

Der letzte Punkt ist der, den die Leute überspringen. Sei nicht diese Person. Starte den Server neu, warte 60 Sekunden und prüfe, ob deine App live ist. Wenn nicht, sind deine Startup-Scripts falsch konfiguriert, und du wirst es zum denkbar schlechtesten Zeitpunkt herausfinden.

Ist das "Enterprise-Grade"?#

Nein. Und das ist der Punkt.

Dieses Setup liefert diesen Blog zuverlässig für unter 10$/Monat aus. Es wird in 30 Sekunden mit einem einzigen Befehl deployt. Ich verstehe jedes einzelne Teil davon. Wenn etwas kaputtgeht, weiß ich genau, wo ich schauen muss.

Könnte ich Docker verwenden? Klar. Könnte ich Kubernetes verwenden? Technisch gesehen. Könnte ich eine vollständige CI/CD-Pipeline mit Staging-Umgebungen und Canary-Deployments einrichten? Absolut.

Aber ich habe gelernt, dass die beste Infrastruktur die ist, die man wirklich versteht, um 2 Uhr morgens debuggen kann und die nicht mehr kostet, als das Projekt einbringt. Für eine persönliche Seite, ein SaaS-MVP oder ein kleines Startup — das ist dieses Setup.

Erst ausliefern. Skalieren, wenn es nötig wird. Und immer, immer, das Deploy-Script auf einem frischen Server testen.