CommunityCodierung & Entwicklunggithub.com

igor-batrakov/new-vps-setup

Schritt-für-Schritt-Sicherheitsleitfaden (Agent-Skill + Checkliste) zur Einrichtung eines neuen Ubuntu VPS für die Produktion für Anfänger: Härtung, automatische Updates, UFW, Fail2ban, Docker, nginx+HTTPS, automatische Backups mit restic, Überwachung.

Was ist new-vps-setup?

Dieser Skill führt einen Entwickler oder Systemadministrator, insbesondere einen Anfänger ohne tiefgehende Erfahrung in der Systemadministration, Schritt für Schritt durch die Einrichtung eines neuen Ubuntu-Servers für die Produktion. Der Fokus liegt auf Sicherheit und Autonomie: Der Server soll sich nach der Einrichtung so weit wie möglich selbst warten. Der Skill deckt die wichtigsten Bereiche ab: grundlegende Systemhärtung (SSH nur mit Schlüsseln, separater sudo-Benutzer), Firewall (UFW), Schutz vor Brute-Force-Angriffen (Fail2ban), automatische Sicherheitsupdates (unattended-upgrades), optionale Installation von Docker mit korrekter Log-Rotation und Netzwerksicherheit, Einrichtung von nginx als Reverse Proxy mit kostenlosem HTTPS über Let's Encrypt, sowie automatisierte, verschlüsselte Backups auf einen externen Speicher (offsite) mit restic. Der Skill betont die Bedeutung von vorherigen Backups vor Änderungen, der Überprüfung jedes Schrittes und der Vermeidung häufiger Anfängerfehler. Er unterstützt Ubuntu 22.04 und 24.04 LTS und ist als detaillierte, ausführbare Anleitung konzipiert, die ein Agent wie Claude Code oder Cursor umsetzen kann.

Funktioniert mit~Claude Code~Codex CLI~Cursor
npx skills add igor-batrakov/new-vps-setup

Installed? Explore more Codierung & Entwicklung skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Einrichtung eines neuen VPS für die Produktion (für Anfänger)

Überblick

Dieser Skill führt Sie Schritt für Schritt durch die Vorbereitung eines frischen Ubuntu-Servers für die Produktion, sodass er danach möglichst autonom verwaltet werden kann. Die Zielgruppe ist eine Person ohne Erfahrung in der Systemadministration. Daher:

  • Sichere Standardeinstellungen, nicht "wie ein Profi". Kein NOPASSWD-sudo, keine deaktivierten Prüfungen aus Bequemlichkeit. Was das Leben eines Experten vereinfacht, ist für einen Anfänger eine Zeitbombe.
  • Autonomie durch Automatisierung. Sicherheitsupdates, Backups und Überwachung müssen von selbst funktionieren. Der Mensch reagiert nur noch auf Alarme.
  • Jeder Schritt wird überprüft. Nach einer Änderung erfolgt eine Überprüfung des Ergebnisses, und erst dann geht es zum nächsten Schritt.

Ziel-Betriebssystem: Ubuntu 22.04 / 24.04 LTS. Die Befehle für andere Distributionen unterscheiden sich.

Goldene Regeln (vor JEDER Änderung)

  1. Backup der Konfiguration: sudo cp /path/config /path/config.bak.$(date +%F-%H%M)
  2. Überprüfe den Zustand: sudo systemctl status <service> / sudo docker ps
  3. Nach der Änderung das Ergebnis überprüfen (Logs, Status, Testverbindung).
  4. Schließe niemals die aktuelle SSH-Sitzung, bevor du in einer neuen Sitzung nicht sichergestellt hast, dass Zugriff und sudo funktionieren. Verlust der SSH-Verbindung zu einem entfernten Server = Ausflug zur Provider-Konsole (VNC).

Prinzipien der Produktionssicherheit (bei allen Schritten einhalten):

  • Geheimnisse (API-Schlüssel, Tokens, Passwörter) nur in .env oder Umgebungsvariablen, niemals im Code.
  • .env muss in .gitignore sein (sonst landen Geheimnisse beim ersten Commit in git).
  • Datenbanken sind nicht im Internet erreichbar – nur localhost oder über ein privates Netzwerk/VPN.
  • Kein Debug-Modus auf dem Server: DEBUG=False, NODE_ENV=production.
  • Anwendungen laufen nicht als root – eigener Benutzer/Container.
  • Keine unnötigen offenen Ports (Admin-Oberflächen, Dashboards, Metriken sind geschlossen oder nur von vertrauenswürdigen IPs erreichbar).
  • Deaktiviere keine Sicherheitsprüfungen aus Bequemlichkeit (verify=False, allowAll – nur lokal).
  • SSH – nur mit Schlüsseln, nicht mit Passwort.

0. Diagnose – was bereits vorhanden ist

Führe dies ZUERST aus. Hosting-Anbieter liefern oft einen Server mit teilweiser Konfiguration (cloud-init, ufw, etc.) aus.

# System
lsb_release -a && uname -r
free -h && df -h && nproc

# Wer bin ich und welche Rechte habe ich
whoami && id

# SSH-Konfiguration (effektive Werte, nicht nur die Dateien!)
sudo sshd -T | grep -iE 'permitrootlogin|passwordauthentication|pubkeyauthentication|^port '

# Firewall
sudo ufw status verbose

# Welche Ports hören
sudo ss -tlnp

# Automatische Updates (aktiviert?)
systemctl status unattended-upgrades 2>/dev/null | head -5
cat /etc/apt/apt.conf.d/20auto-upgrades 2>/dev/null

# Docker / fail2ban
sudo docker ps 2>/dev/null
sudo fail2ban-client status 2>/dev/null

Nach der Diagnose informiere den Benutzer über das Gefundene und beginne nicht mit der Einrichtung ohne Bestätigung.


1. Ersteinrichtung und Sicherheit

Vergleiche mit Abschnitt 0 – ein Teil könnte bereits erledigt sein. Mache nichts doppelt.

1.1 Systemaktualisierung

sudo apt update && sudo apt upgrade -y
sudo apt autoremove -y
# Wenn der Kernel aktualisiert wurde, ist ein Reboot erforderlich (siehe Ausgabe). Starte bewusst neu:
# sudo reboot   # und verbinde dich nach ~30-60s wieder

1.2 Separater sudo-Benutzer (NICHT als root arbeiten)

sudo adduser <BENUTZERNAME>            # fragt nach einem Passwort – wähle ein sicheres, speichere es im Passwort-Manager
sudo usermod -aG sudo <BENUTZERNAME>

Für Anfänger lassen wir sudo mit Passwortabfrage. Dies ist beabsichtigt: NOPASSWD-sudo entfernt die letzte Barriere bei Kompromittierung des Schlüssels oder der Sitzung. Das sudo-Passwort wird auch als Fallback benötigt, falls der SSH-Schlüssel verloren geht (siehe 1.3).

1.3 SSH-Schlüssel (Anmeldung ohne Passwort mit Schlüssel)

Schlüsseltyp – ed25519 (nicht RSA). Name – <server>_<geraet>, mit Passphrase.

Auf dem lokalen Rechner:

ssh-keygen -t ed25519 -f ~/.ssh/<server>_<geraet> -C "<server>_<geraet>"
ssh-copy-id -i ~/.ssh/<server>_<geraet>.pub <BENUTZERNAME>@<SERVER_IP>

Überprüfe die Anmeldung mit dem Schlüssel in einer SEPARATEN Sitzung (die alte nicht schließen!):

ssh -i ~/.ssh/<server>_<geraet> -o IdentitiesOnly=yes <BENUTZERNAME>@<SERVER_IP> 'whoami; sudo whoami'

Praktischer Alias in ~/.ssh/config auf dem lokalen Rechner:

Host <alias>
    HostName <SERVER_IP>
    User <BENUTZERNAME>
    IdentityFile ~/.ssh/<server>_<geraet>
    IdentitiesOnly yes

1.4 SSH-Härtung

Erstelle /etc/ssh/sshd_config.d/99-hardening.conf:

PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
AllowTcpForwarding local
ClientAliveInterval 300
ClientAliveCountMax 2
AllowUsers <BENUTZERNAME>

AllowTcpForwarding local erlaubt lokale Weiterleitung (ssh -L – um einen Dienst auf 127.0.0.1 vor der Domain-Konfiguration zu erreichen) und verbietet entfernte (-R). Wenn keine Weiterleitung benötigt wird – setze no.

Anwenden und überprüfen:

sudo sshd -t                                              # Syntax OK?
sudo systemctl restart ssh
sudo sshd -T | grep -iE 'permitroot|passwordauth'         # effektive Werte = no

Gotcha 1 – cloud-init überschreibt die Härtung. Die Datei /etc/ssh/sshd_config.d/50-cloud-init.conf enthält oft PasswordAuthentication yes und in sshd_config.d/ gilt „first match wins". Überprüfe mit sudo sshd -T (effektive Werte), nicht nur deine Datei. Wenn es überschreibt – korrigiere/entferne die Zeile in 50-cloud-init.conf.

Gotcha 2 – Ubuntu 24.04 verwendet ssh.socket. Port in sshd_config wird ignoriert. Um den SSH-Port zu ändern: sudo systemctl edit ssh.socket → füge ListenStream=<PORT> hinzu (und entferne den Standard-Port). Überprüfe über die Provider-Konsole – ein Fehler hier blockiert den Zugriff.

1.5 Automatische Sicherheitsupdates ⭐ (Kern der Autonomie)

Dies ist der wichtigste Schritt für „der Server verwaltet sich selbst". Ohne ihn sammeln sich Sicherheitslücken in Paketen an.

sudo apt install unattended-upgrades apt-listchanges -y
sudo dpkg-reconfigure -plow unattended-upgrades   # wähle "Yes" – erstellt 20auto-upgrades

Überprüfe /etc/apt/apt.conf.d/20auto-upgrades:

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";

In /etc/apt/apt.conf.d/50unattended-upgrades (kommentiere ein/konfiguriere die wichtigsten Zeilen):

// Sicherheitsupdates installieren (standardmäßig aktiviert):
Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
    "${distro_id}ESMApps:${distro_codename}-apps-security";
    "${distro_id}ESM:${distro_codename}-infra-security";
};
Unattended-Upgrade::Remove-Unused-Dependencies "true";
// Automatischer Neustart, wenn das Update es erfordert (Kernel). Wähle ein Fenster mit minimaler Last:
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";

Entscheidung „Automatischer Neustart": true = Der Server startet nachts bei einem Kernel-Update neu (sicherer, aber kurze Ausfallzeit möglich). false = Der Kernel wird aktualisiert, aber der Reboot wartet auf dich (muss überwacht werden). Für einen einzelnen Produktionsserver eines Anfängers ist meist true + nächtliches Fenster besser.

Überprüfung – Trockenlauf ohne Installation:

sudo unattended-upgrade --dry-run --debug 2>&1 | tail -20
systemctl list-timers 'apt-daily*'   # Timer aktiv?

1.6 fail2ban (Schutz von SSH vor Brute-Force)

Die Basiskonfiguration reicht aus – verkompliziere es nicht mit recidive/nftables am Anfang.

sudo apt install fail2ban -y

/etc/fail2ban/jail.d/sshd.local:

[sshd]
enabled = true
bantime = 1h
findtime = 30m
maxretry = 4
# Nicht sich selbst bannen: Füge deine STATISCHEN IPs hinzu (ermitteln: curl ifconfig.me)
ignoreip = 127.0.0.1/8 ::1 <DEINE_HEIMAT_IP>
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd

1.7 Zeitzone und (für kleine VPS) Swap

sudo timedatectl set-timezone <Region/Stadt>     # z. B. Europe/Berlin oder UTC

# Swap wird auf VPS mit wenig RAM (< 2 GB) benötigt, damit OOM keine Dienste tötet:
free -h
sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile
sudo mkswap /swapfile && sudo swapon /swapfile
grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swap.conf && sudo sysctl --system

2. Firewall UFW

Prinzip: alles geschlossen, nur das Nötige geöffnet. Erlaube zuerst SSH, sonst schließt du dich selbst aus.

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp comment 'SSH'      # WICHTIG: vor enable!
sudo ufw enable
sudo ufw status verbose

Für den Webserver füge HTTP/HTTPS hinzu:

sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'

Admin-Oberflächen/Dashboards/Metriken – nur von vertrauenswürdigen IPs, nicht für „alle":

sudo ufw allow from <VERTRAUENS_IP> to any port <ADMIN_PORT> proto tcp comment 'Admin-Panel'

Verwende niemals sudo ufw reset – es setzt alle Regeln zurück, einschließlich SSH.


3. Docker (optional – wenn Anwendungen in Containern)

Installation

curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker <BENUTZERNAME>      # neu anmelden, damit die Gruppe wirksam wird

Die Mitgliedschaft in der Gruppe docker ist praktisch gleichbedeutend mit root (über docker run -v /:/host). Dies ist für einen einzelnen Administrator akzeptabel, aber gib diese Gruppe nicht Benutzern, denen du nicht den vollen Zugriff auf den Server anvertraust.

Log-Rotation (zwingend erforderlich – sonst fressen Logs die Festplatte)

/etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}
sudo systemctl restart docker

⚠️ Docker umgeht UFW

Ein als -p 8080:8080 weitergeleiteter Port ist aus dem Internet erreichbar, selbst wenn UFW ihn nicht erlaubt hat. Sichere Optionen:

  • Nur auf localhost binden: -p 127.0.0.1:8080:8080 (nur über nginx/SSH-Tunnel erreichbar).
  • Datenbank-Ports überhaupt nicht nach außen leiten – Kommunikation über Dienstnamen innerhalb des Docker-Netzwerks.

4. nginx + HTTPS (optional – wenn eine Webanwendung und eine Domain vorhanden sind)

nginx als Reverse Proxy vor der Anwendung (die Anwendung hört auf 127.0.0.1, nach außen nur nginx).

sudo apt install nginx -y

/etc/nginx/sites-available/<app>:

server {
    listen 80;
    server_name <DOMAIN>;
    location / {
        proxy_pass http://127.0.0.1:<APP_PORT>;
        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;
    }
}
sudo ln -s /etc/nginx/sites-available/<app> /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

HTTPS kostenlos mit Let's Encrypt (certbot erstellt und konfiguriert die automatische Verlängerung selbst):

sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d <DOMAIN>
# Automatische Verlängerung ist bereits als Timer eingerichtet. Überprüfung:
sudo certbot renew --dry-run

Schutz: Füge server_tokens off; in /etc/nginx/nginx.conf hinzu (nginx-Version verbergen) und Sicherheits-Header (Strict-Transport-Security, X-Content-Type-Options nosniff).


5. Automatische Backups (restic + extern)

Vollständige Anleitung (Installation, Repository, Skript, Timer, Wiederherstellung): lies references/backups.md – dort Schritt für Schritt.

Wichtige Prinzipien, die NICHT verletzt werden dürfen:

  1. Das Backup MUSS extern sein. Eine Kopie auf demselben Server (oder derselben Festplatte) hilft nicht bei Server-/Festplattenausfall. Minimum – externer Speicher (S3-kompatibel: Backblaze B2, rsync.net usw.).
  2. Das Verschlüsselungspasswort des Repos AUßERHALB des Servers speichern (Passwort-Manager). Passwort verloren → alle Backups für immer unlesbar. Dies ist der häufigste Weg, ohne Backup dazustehen.
  3. Ein Backup ohne Wiederherstellungstest ist kein Backup. Führe mindestens einmal eine Testwiederherstellung durch (siehe backups.md, „Test-Wiederherstellung").
  4. Automatisierung: Täglicher Start über systemd-Timer + Rotation alter Snapshots.

Was sichern

Denke in Kategorien, nicht in einzelnen Pfaden – so vergisst du nichts:

KategorieBeispielpfadeWozu
Systemkonfiguration/etc komplettnginx, ssh, ufw, fail2ban, systemd-Units – um den Server „wie er war" wiederherzustellen
Anwendungsdaten/home/<BENUTZERNAME>/app/data, /var/lib/<service>Was nicht neu installiert werden kann: Benutzerinhalte, Uploads
DB-Dumps/var/backups/*.sql.gzDump via pg_dump/.backup vor restic (die Live-DB-Datei kann beschädigt sein)
Geheimnisse.env, /root/.config/*Werden verschlüsselt gesichert (restic verschlüsselt das Repo). Verschlüsselungspasswort – außerhalb des Servers

Was NICHT gesichert wird (wird neu installiert, bläht nur das Backup auf): node_modules, .venv/venv, Docker-Images (docker pull), Systempakete (apt), Caches, /var/log, /tmp. Diese Pfade sind bereits im --exclude des Skripts enthalten (siehe backups.md).

Wie lange aufbewahren (Retention) – wähle eine Richtlinie

restic speichert deduplizierte Snapshots, daher sind „mehr Kopien" platzsparend. Befehl: restic forget --prune --keep-daily N --keep-weekly N --keep-monthly N.

RichtlinieBefehlFür wen geeignetBegründung
Basis (Standard)--keep-daily 7 --keep-weekly 4 --keep-monthly 6Die meisten ProduktionsserverRollback auf jeden der letzten 7 Tage + Verlauf bis zu einem halben Jahr. Erfasst sowohl frische Fehler als auch „wann ist das kaputtgegangen". Steht bereits in backups.md.
Leicht--keep-daily 7 --keep-weekly 4Websites/Bots ohne wertvolle kumulative Daten~1 Monat Verlauf. Minimaler Platzbedarf, einfacher. Geeignet, wenn der Verlust alter Daten nicht kritisch ist.
Lang--keep-daily 14 --keep-weekly 8 --keep-monthly 12DBs mit wichtigen Daten, Finanzen, rechtliche AnforderungenEin Jahr Verlauf + 2 Wochen täglich. Teurer im Speicher, schützt aber vor „stiller" Datenbeschädigung, die nicht sofort bemerkt wird.

Empfehlung für Anfänger: Beginne mit der Basis – sie deckt beide Szenarien (schnelles Rollback und langer Verlauf) bei geringen Speicherkosten ab. Ein Wechsel zur Langen ist jederzeit möglich, indem du nur die Zahlen in forget änderst – alte Snapshots gehen nachträglich nicht verloren.

Wichtig zu DBs: Retention ist sinnlos, wenn du die „Live"-DB-Datei sicherst – sie kann beschädigt sein. Erstelle immer zuerst einen Dump (pg_dump / SQLite .backup), und restic sichert den Dump.


6. Überwachung

…

[gekürzt für die Übersetzung]

Verwandte Skills