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)
- Backup der Konfiguration:
sudo cp /path/config /path/config.bak.$(date +%F-%H%M) - Überprüfe den Zustand:
sudo systemctl status <service>/sudo docker ps - Nach der Änderung das Ergebnis überprüfen (Logs, Status, Testverbindung).
- 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
.envoder Umgebungsvariablen, niemals im Code. .envmuss in.gitignoresein (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 localerlaubt lokale Weiterleitung (ssh -L– um einen Dienst auf127.0.0.1vor der Domain-Konfiguration zu erreichen) und verbietet entfernte (-R). Wenn keine Weiterleitung benötigt wird – setzeno.
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 meisttrue+ 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
dockerist praktisch gleichbedeutend mit root (überdocker 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:
- 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.).
- 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.
- Ein Backup ohne Wiederherstellungstest ist kein Backup. Führe mindestens einmal eine Testwiederherstellung durch (siehe backups.md, „Test-Wiederherstellung").
- Automatisierung: Täglicher Start über systemd-Timer + Rotation alter Snapshots.
Was sichern
Denke in Kategorien, nicht in einzelnen Pfaden – so vergisst du nichts:
| Kategorie | Beispielpfade | Wozu |
|---|---|---|
| Systemkonfiguration | /etc komplett | nginx, 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.gz | Dump 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.
| Richtlinie | Befehl | Für wen geeignet | Begründung |
|---|---|---|---|
| Basis (Standard) | --keep-daily 7 --keep-weekly 4 --keep-monthly 6 | Die meisten Produktionsserver | Rollback 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 4 | Websites/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 12 | DBs mit wichtigen Daten, Finanzen, rechtliche Anforderungen | Ein 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]