Linux-Admin-Befehle

104 Befehle, die man wirklich tippt — jede Option erklärt, nach Vorhaben gruppiert

📁 Dateien und Verzeichnisse (8)

Suchen, stapelweise bearbeiten, packen — ein falscher Parameter kostet meist eine gelöschte Datei oder übersehene versteckte Verzeichnisse

ls: Rechte, Zeiten und Größen lesen

-l Langformat: Rechte, Eigentümer, Größe, Zeit
-a auch . und .. sowie alle versteckten Dateien zeigen
-h Größen mit Einheit (1.2K statt 1234), nur zusammen mit -l
-t nach Änderungszeit absteigend, neueste zuerst
-r umgekehrte Sortierung, mit -t also älteste zuerst
-S nach Dateigröße absteigend — bei der Suche nach Platzfressern direkter als -t
--time-style=long-iso Zeit als 2026-10-29 09:18, besser lesbar als der Standard

# neueste Datei oben, mit Größe und Uhrzeit
ls -laht --time-style=long-iso

# die größten Speicherfresser nach Größe finden (-S)
ls -lhS /var/log | head

find: Dateien nach Bedingungen finden und behandeln

-name Dateiname, Groß-/Kleinschreibung zählt; -iname ignoriert sie
-type f nur reguläre Dateien, -type d nur Verzeichnisse, -type l Symlinks
-mtime +7 älter als 7 Tage; -mmin -30 in den letzten 30 Minuten geändert
-size +50M größer als 50M; -size -1k kleiner als 1k
-maxdepth 1 nicht rekursiv — beim Debuggen zuerst setzen, oft eine Größenordnung schneller
-ls führt ls -dils aus: Größe, Inode und Zeit in einem Durchgang
-delete löscht direkt, nur leere Verzeichnisse; -exec ... \; führt pro Treffer ein Kommando aus
-prune überspringt ein Verzeichnis, nur zusammen mit -path sinnvoll
-o bedeutet ODER beim Verknüpfen von Bedingungen; -print erzwingt Ausgabe (Pflicht mit -prune)

# große Dateien, in den letzten 30 Minuten geändert (Diagnose plötzlich vollaufender Platte)
find /var -type f -mmin -30 -size +50M -ls

# Logs, die älter als 7 Tage sind, gesammelt löschen
find /var/log -name '*.log.*' -mtime +7 -delete

# .git und node_modules überspringen und dann suchen
find . -path ./node_modules -prune -o -name '*.js' -print

grep: rekursiv suchen mit Kontext

-r rekursiv; -R rekursiv und Symlinks folgen
-i Groß-/Kleinschreibung ignorieren; -n Zeilennummern; -v invertiert, nur nicht passende Zeilen
-E erweiterter Regex (| braucht -E, sonst gilt es wörtlich)
-l nur Dateinamen, gut zum Eingrenzen vor dem Lesen
-A3 / -B3 / -C3 drei Zeilen danach / davor / davor und danach
--include='*.conf' nur Treffer dieser Endung; --exclude-dir Verzeichnisse überspringen
-w ganzes Wort, damit cat nicht auch category trifft
-rn ist das Alltagspaar: rekursiv plus Zeilennummern, sonst findet man die Zeile nicht

# in der nginx-Konfiguration ein upstream suchen, mit je 3 Zeilen davor und danach
grep -rn -C3 'upstream' /etc/nginx --include='*.conf'

# Treffer bei einem von mehreren Suchbegriffen (erfordert -E)
grep -rnE 'timeout|refused|reset' /var/log/app/error.log

# nur Dateinamen mit Treffern auflisten, dann einzeln ansehen
grep -rli 'proxy_pass' /etc/nginx

tar: packen und entpacken

-c erzeugen; -x entpacken; -t nur anzeigen; -z über gzip; -j über bzip2
-f nimmt den Archivnamen, gehört deshalb immer ans Ende
-v listet den Fortschritt, bremst große Archive stark — beim Skripten umleiten
-C in ein Verzeichnis entpacken, das bereits existieren muss
--exclude beim Packen überspringen; das Muster gilt für relative Pfade
-czf / -xzf / -tzf sind die drei Kombinationen, die man kennen sollte
Vor dem Entpacken immer -t laufen lassen — es verhindert, dass eine Tarbomb das aktuelle Verzeichnis überschreibt

# packen, Cache-Verzeichnisse ausschließen
tar -czf app.tar.gz --exclude='node_modules' --exclude='.git' -C /srv app

# erst Inhalt ansehen, dann in das Zielverzeichnis entpacken
tar -tzf app.tar.gz | head -20
tar -xzf app.tar.gz -C /tmp/restore

cp / mv / rm: drei gefährliche Optionen

rm -rf rekursiv und erzwungen, ohne Nachfrage — bei leerer Variable wird rm -rf "$DIR/" zu rm -rf /
-f erzwingt ohne Rückfrage; -r geht in Verzeichnisse — zusammen das gefährlichste Paar
cp -a Archivmodus: behält Rechte, Zeiten, Symlinks, die richtige Option für Backups
cp -n überschreibt nichts (no-clobber); mv -n genauso
mv über Dateisystemgrenzen ist Kopieren plus Löschen, nicht atomar — ein Abbruch lässt beide Seiten unvollständig
Von rm gelöschte Dateien belegen weiter Platz, solange ein Prozess sie hält (siehe Plattenanalyse)
test -n "$DIR" prüft auf nicht-leer, vor jedem rm Pflicht
-- beendet die Optionsauswertung, damit eine Datei mit - am Anfang nicht als Option gilt

# sicheres Löschen: erst prüfen, dass die Variable nicht leer ist, dann löschen
[ -n "$DIR" ] && rm -rf -- "$DIR"

# vollständige Sicherung mit erhaltenen Attributen
cp -a /srv/app /backup/app-$(date +%F)

du / df: wer hat die Platte gefüllt

df -h Auslastung je Mountpoint; -i Inode-Auslastung (volle Inodes blockieren Schreibzugriffe ebenfalls)
du -sh Summe eines Verzeichnisses; -s nur Gesamtsumme, -h human readable
du -sh * | sort -h größte Einträge zuerst, sort -h versteht 1K/2M/3G
--max-depth=1 nur eine Ebene, deutlich schneller als volle Rekursion
Weichen du und df stark ab, sind es fast immer gelöschte, aber noch offene Dateien
2>/dev/null unterdrückt Permission-Fehler, die die Ausgabe sonst fluten

# von der Wurzel schichtweise nach unten, bei einem plötzlichen Sprung hineingehen
du -h --max-depth=1 / 2>/dev/null | sort -h | tail -10

# gelöschte, aber von Prozessen gehaltene Dateien (wenn df voll, du aber klein)
lsof +L1 2>/dev/null | head

ln: symbolische und harte Links

ln -s ZIEL LINKNAME — das Ziel kommt zuerst, sonst entsteht ein toter Link
-s symbolischer Link; ohne -s ein Hardlink (nur gleiches Dateisystem, nicht für Verzeichnisse)
-f überschreibt einen vorhandenen Link
-n zusammen mit -f: einen Link auf ein Verzeichnis ersetzen statt darin anzulegen
-sfn ist die Standardkombination für den Release-Wechsel (current zeigt auf das neue Release)
Symlinks speichern einen Pfad und brechen beim Verschieben des Ziels; Hardlinks teilen einen Inode

ln -sfn /srv/app/releases/20261029 /srv/app/current

stat / file: was eine Datei wirklich ist

stat liefert Größe, Inode, Rechte und drei Zeiten — die direkte Antwort auf „wurde die Datei wirklich geändert“
-c eigenes Format: %y Änderungszeit, %s Bytes, %U:%G Eigentümer, %a Oktalrechte, %n Dateiname
-L folgt dem Symlink und beschreibt das Ziel (ohne -L beschreibt es den Link selbst)
Drei Zeiten: Access gelesen, Modify Inhalt geändert, Change Metadaten (Rechte/Eigentümer) geändert
-f Informationen auf Dateisystemebene (Blockgröße, Inodes), ergänzt df
touch -d '2026-10-01 08:00' DATEI setzt die Änderungszeit; mit find -newer Änderungen eingrenzen
file nennt den echten Typ, verlässlicher als die Endung — die lässt sich beliebig ändern
stat -c '%a' ist der schnellste Weg, die tatsächlichen Rechtebits zu prüfen

stat -c '%y %s %U:%G %a %n' /etc/nginx/nginx.conf
stat -L -c '%y %s' /srv/app/current        # die echte Datei hinter dem Symlink anzeigen
file /usr/local/bin/app

✂️ Textverarbeitung und Pipes (8)

Konfiguration ändern, Spalten auswerten, Kommandos bauen — alles, was 200 Zeilen berührt, gehört zu sed/awk

sed: Dateiinhalte ändern (auch in place)

-i ändert die Datei direkt (in place) — in Produktion erst ohne -i laufen lassen und die Ausgabe prüfen
-i.bak legt beim Ändern eine .bak-Kopie an, sofortiger Rollback möglich (zur Gewohnheit machen)
-n druckt nicht automatisch, mit p nur die gewünschten Zeilen — entspricht „bestimmte Zeilen ansehen“
-E erweiterter Regex (+ ? | ( ) ohne Escaping); auf alten Systemen -r, gleiche Wirkung
-e verkettet Ausdrücke, mehrere gehen auch per Semikolon in einem Ausdruck
Das g in s/alt/neu/g ersetzt alle Treffer einer Zeile, ohne g nur den ersten
Adressbereiche: '/^server/,/}/ s/a/b/' ändert nur innerhalb eines Config-Blocks
\1 \2 referenzieren Capture-Gruppen, & steht für den gesamten Treffer
Reihenfolge beachten: sed -E -i.bak 's/x/y/' DATEI

sed -n '1,20p' /etc/nginx/nginx.conf              # nur die ersten 20 Zeilen anzeigen
sed -i.bak 's/listen 80;/listen 8080;/' /etc/nginx/sites-enabled/app
sed -E -i.bak 's/^server_name[[:space:]]+([^;]+);/server_name \1.bak;/' app.conf
sed '/^[[:space:]]*#/d;/^[[:space:]]*$/d' app.conf | head        # Kommentare und Leerzeilen entfernen

awk: Spalten lesen und summieren

-F setzt das Trennzeichen: -F: Doppelpunkt, -F'\t' Tabulator, ohne Angabe Folgen von Leerzeichen
'{print $1,$3}' nimmt Spalte 1 und 3; $0 ist die ganze Zeile; NF die Feldzahl; NR die Zeilennummer
Filter stehen vor dem Programm: '$9==500' heißt Spalte 9 gleich 500
Numerisch vergleichen mit +0: $9+0 >= 500, sonst vergleicht awk als String und überrascht
'{s+=$7} END {print s}' ist die Summenvorlage; zählen mit '{c++} END {print c}'
Array-Statistik: {c[$1]++} END {for (k in c) print k, c[k]}
-v übergibt Variablen: awk -v lim=500 '$3>lim', -v steht vor dem Programm
-F und -v wertet awk selbst aus, sie müssen vor dem Programm in Anführungszeichen stehen
In den Beispielen: sort -rn numerisch absteigend, uniq -c zählt, ss -tn listet TCP-Verbindungen

awk -F: '$3>=1000 {print $1, $3}' /etc/passwd
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
awk -v lim=500 '{ if ($9+0 >= lim) print NR": "$7 }' access.log
ss -tn | awk 'NR>1 {c[$1]++} END {for (k in c) print k, c[k]}'

sort / uniq: sortieren, entdoppeln, Top N

sort -n numerisch sortieren (lexikografisch steht 10 vor 9)
sort -h versteht 1K/2M/3G — bei du- und df-Ausgaben Pflicht
sort -r umgekehrt; -u sortiert und entfernt Duplikate in einem Durchgang
-k2 sortiert nach Feld 2, -k3 analog; mehrere Schlüssel als -k1,1 -k2,2
-t: setzt das Trennzeichen auf Doppelpunkt (Standard ist Whitespace)
uniq -c zählt aufeinanderfolgende Wiederholungen — daher erst sort, sonst zerfallen gleiche Werte in Blöcke
uniq -d nur doppelte Zeilen, uniq -u nur eindeutige
Das Top-N-Muster: sort | uniq -c | sort -rn | head
Im Beispiel du -sh: -s liefert nur die Summe statt Einzelzeilen (siehe du / df)

du -sh /var/log/* | sort -h | tail -5
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10
sort -t: -k3 -n /etc/passwd | head

cut / tr: Spalten schneiden, Zeichen tauschen

cut -d: -f1,7 nimmt Felder per Trennzeichen: -d setzt es, -f wählt die Felder (mehrere möglich)
cut -c1-19 schneidet nach Zeichenposition, gut für den Zeitstempel in Logs fester Breite
cut kennt nur ein Trennzeichen; mit Leerzeichen ausgerichtete Ausgaben zerlegt es falsch, dann awk nehmen
tr 'a-z' 'A-Z' bildet Zeichen eins zu eins ab; für String-Ersetzung sed verwenden
tr -d löscht Zeichen, meist tr -d '\r' gegen Windows-Zeilenenden
tr -s fasst wiederholte Zeichen zu einem zusammen, etwa für Folgen von Leerzeichen
tr liest nur von stdin, keine Dateinamen, daher mit < umleiten
In den Beispielen: df -h human readable, cut -f5 nimmt Feld 5, sort -u entdoppelt

cut -d: -f1,7 /etc/passwd | head
cut -c1-19 /var/log/nginx/access.log | head -3
tr -d '\r' < crlf.txt > lf.txt
df -h | tr -s ' ' | cut -d' ' -f5 | sort -u

xargs: Ausgabe zu Argumenten machen

xargs macht aus jeder stdin-Zeile ein Argument des folgenden Kommandos — so entsteht find | xargs rm
-0 gehört zu find -print0: Dateinamen mit Leerzeichen oder Zeilenumbruch werden nicht zerlegt (Pflicht bei Uploads)
find -print0 beendet jeden Treffer mit \0, genau was xargs -0 erwartet
-n1 übergibt ein Argument pro Aufruf, sicherer, und man sieht, welches Element scheitert
-I {} ist ein Platzhalter, das Argument steht mitten im Kommando: xargs -I {} cp {} /backup
-p fragt vor der Ausführung, -t gibt erst aus — bei Unsicherheit zuerst -t verwenden
-r führt bei leerer Eingabe nichts aus (GNU), sonst löst eine leere Liste ein rm aus
-P4 vier Prozesse parallel, bei Batch-Kompression oder Downloads mehrere Male schneller
cp --parents kopiert die Verzeichnisstruktur mit, nützlich beim Wiederherstellen von Backups
In den Beispielen filtern find -name / -mtime / -type, und rm -f erzwingt ohne Rückfrage

find /var/log -name '*.log.*' -mtime +7 -print0 | xargs -0 -r rm -f
find /srv/uploads -type f -print0 | xargs -0 -I {} cp --parents {} /backup
printf '%s\n' a b c | xargs -n1 -t echo

tee: zusehen und mitschreiben

tee schreibt die Eingabe gleichzeitig in Datei und stdout — ideal, um vor Konfigurationsänderungen Belege zu sichern
-a hängt an statt zu überschreiben, für Logs immer verwenden
Warum sudo tee: bei sudo echo x > /etc/f macht die aktuelle Shell die Umleitung und scheitert an den Rechten
-i ignoriert Unterbrechungssignale, ein langer Mitschnitt wird nicht per Ctrl-C abgebrochen
Vor der Pipe 2>&1 einbauen, sonst erreichen Fehlermeldungen tee nie
Zeitstempel ergänzen: ... | ts oder | awk '{print strftime("%F %T"), $0}'

./deploy.sh 2>&1 | tee -a /var/log/deploy-$(date +%F).log
echo 'net.ipv4.ip_forward=1' | sudo tee -a /etc/sysctl.conf

jq: JSON-Antworten lesen

jq liest JSON strukturell statt zeilenweise, was grep nicht sicher kann
. liest Felder: .data .items[0].name; .[] durchläuft Arrays
-r gibt reine Strings ohne Anführungszeichen aus, Pflicht für Variablen oder weitere Pipes
-c legt jedes Ergebnis auf eine Zeile, praktisch für sort / uniq
'keys' listet die Felder eines Objekts, 'length' die Array-Länge, '..' alle Pfade (bei unbekannter Struktur)
select() filtert: '.[] | select(.status != "ok")'
-C farbige Ausgabe für Menschen, -M einfarbig für Dateien und Skripte
jq formatiert standardmäßig; -e macht aus dem Ergebnis einen Exit-Code für Skripte
Im Beispiel unterdrückt curl -s Fortschrittsausgaben, die das JSON zerstören würden

curl -s https://api.example.com/v1/tools | jq -r '.data[].name'
curl -s 'https://api.example.com/v1/list?page=1' | jq -c '.items[] | {id, name}'
jq -r '.[] | select(.status != "ok") | .id' resp.json

diff / wc: vergleichen und zählen

diff -u vereinheitlichtes Format, also Patch-Format, direkt von patch verwendbar
diff -r vergleicht zwei Bäume; -q meldet nur, welche Dateien sich unterscheiden
diff -y zeigt nebeneinander, -W 200 setzt die Breite — gut für den Blick auf Konfigurationen
wc -l Zeilen (die übliche Log-Kennzahl); -c Bytes; -w Wörter
wc gibt den Dateinamen mit aus, im Skript daher awk '{print $1}'
comm eignet sich für Mengendifferenzen zweier sortierter Listen
comm -13 a b zeigt Zeilen nur aus b; die Spaltenlogik vor dem Vertrauen prüfen
Beide Seiten vorher sortieren und entdoppeln, sonst ist „alles anders“ nur andere Reihenfolge

diff -u /etc/nginx/nginx.conf.bak /etc/nginx/nginx.conf | head -40
diff -rq /srv/app/releases/20261028 /srv/app/releases/20261029
wc -l /var/log/nginx/access.log
sort a.txt > a.s; sort b.txt > b.s; comm -13 a.s b.s   # nur Zeilen, die ausschließlich in b stehen

🖥️ Remote-Login und Dateitransfer (8)

Login, Schlüssel, Übertragung, Fortsetzen — der meiste Zeitaufwand liegt meist darin, die Datei überhaupt dorthin zu bekommen

ssh: auf einem Remote-Server anmelden

ssh benutzer@host ist der Standard-Login; Port 22 voreingestellt
-p 2222 setzt den Port (Achtung: scp und sftp nutzen -P, nicht verwechseln)
-i verweist auf einen privaten Schlüssel; sonst werden ~/.ssh/id_ed25519 und id_rsa probiert
-t erzwingt ein Pseudo-Terminal — nötig für sudo, top und andere interaktive Kommandos
-v zeichnet Handshake und Authentifizierung auf und zeigt, ob es am Netz oder an der Auth hängt; -vvv detailreicher
-o reicht Optionen durch: -o ConnectTimeout=5 gegen Hänger, -o BatchMode=yes verbietet jede Rückfrage (Pflicht im Skript)
Ein Remote-Kommando ohne Shell: ssh HOST 'cmd' — Grundlage für Sammelprüfungen
-J springt über eine Bastion: ssh -J bastion@1.2.3.4 deploy@10.0.0.5 (alte Form: -o ProxyCommand)
Im Beispiel bedeutet df -h human readable (siehe du / df)

ssh -p 2222 -i ~/.ssh/id_ed25519 deploy@10.0.0.5
ssh -o ConnectTimeout=5 -o BatchMode=yes 10.0.0.5 'uptime; df -h /'
ssh -v -t 10.0.0.5 'sudo systemctl restart app'
ssh -J bastion@1.2.3.4 deploy@10.0.0.5

ssh-keygen / ssh-copy-id: Login ohne Passwort

ssh-keygen -t ed25519 erzeugt ein Schlüsselpaar (kürzer und schneller als rsa, modern überall unterstützt)
-t wählt den Typ: ed25519 bevorzugt, rsa nur für alte Systeme, dann mit -b 4096 für die Länge
-C schreibt einen Kommentar (Zweck oder Mail), damit mehrere Schlüssel unterscheidbar bleiben
-f benennt die Ausgabedatei und verhindert das Überschreiben des Standardschlüssels
-N '' setzt eine leere Passphrase für Automatisierung; interaktiv genügt Enter
ssh-copy-id hängt den Public Key an ~/.ssh/authorized_keys und korrigiert die Rechte von Verzeichnis und Datei
-i (von ssh-copy-id) wählt den zu kopierenden Public Key, sonst wird id_*.pub genommen
-R löscht einen veralteten Host-Fingerprint aus known_hosts — nach jeder Neuinstallation nötig, sonst REMOTE HOST IDENTIFICATION HAS CHANGED
-lf zeigt Fingerprint und Typ, um zu prüfen, ob beide Maschinen denselben Schlüssel haben
Private Keys brauchen 600, das Verzeichnis 700, sonst verweigert ssh die Nutzung

ssh-keygen -t ed25519 -C 'deploy@icetools' -f ~/.ssh/id_deploy -N ''
ssh-copy-id -i ~/.ssh/id_deploy.pub deploy@10.0.0.5
ssh-keygen -R 10.0.0.5
ssh-keygen -lf ~/.ssh/id_deploy.pub

~/.ssh/config: Aliasse und Sprünge

Danach ist ssh app gleich ssh -p 2222 deploy@10.0.0.5 — kein langer Befehl mehr
Host ist der Alias und der Geltungsbereich aller folgenden Einstellungen
HostName echte Adresse; User Login-Benutzer; Port Port; IdentityFile Pfad zum Schlüssel
IdentitiesOnly yes bietet nur diesen Schlüssel an, statt den Agenten ein Dutzend probieren zu lassen (zu viele Versuche sperren aus)
ProxyJump ist die Bastion, entspricht -J, und scp / rsync erben sie automatisch
ServerAliveInterval 30 sendet alle 30 Sekunden einen Heartbeat, NAT und Cloud-Loadbalancer kappen Idle-Verbindungen
ServerAliveCountMax 3 trennt erst nach drei fehlgeschlagenen Heartbeats, passend zum Intervall
Host * steht am Dateiende und trägt globale Vorgaben (Heartbeat, Timeouts)
Rechte: config 600, ~/.ssh 700, sonst kommt Bad owner or permissions

# ~/.ssh/config  (chmod 600)
Host app
    HostName 10.0.0.5
    User deploy
    Port 2222
    IdentityFile ~/.ssh/id_deploy
    IdentitiesOnly yes
    ServerAliveInterval 30
    ServerAliveCountMax 3

Host db
    HostName 10.0.0.6
    ProxyJump app

Host *
    ServerAliveInterval 30

SSH-Tunnel: Portweiterleitung

-L lokale Weiterleitung: ssh -L 8080:127.0.0.1:80 HOST bildet Remote-Port 80 auf lokal 8080 ab
-R Rückwärtsweiterleitung: gibt einen lokalen Port auf der Gegenseite frei, damit außen ein interner Dienst erreichbar ist
-D dynamische Weiterleitung erzeugt einen SOCKS-Proxy (mit curl --socks5 oder im Browser)
-N nur weiterleiten, keine Shell; -f in den Hintergrund — fast immer gemeinsam
-g erlaubt anderen Hosts den lokalen Weiterleitungsport (Standard ist nur 127.0.0.1)
Die Mitteladresse in -L wird aus Sicht der Gegenseite aufgelöst; 127.0.0.1 meint dort den Remote-Host selbst
Für „Datenbank ist nicht direkt erreichbar“: Port per -L lokal holen, dann auf 127.0.0.1 verbinden
Ist der lokale Port belegt, einen anderen nehmen; lsof -i :8080 zeigt den Halter

ssh -N -f -L 15432:127.0.0.1:5432 deploy@10.0.0.5
ssh -N -L 8080:10.0.0.6:80 app
ssh -D 1080 -N -f app
ssh -g -L 8080:127.0.0.1:80 app

scp: Dateien hoch- und herunterladen

scp QUELLE ZIEL — die Richtung entscheidet: lokal zuerst ist Upload, remote zuerst Download
-P (groß) setzt den Port (ssh nutzt -p; die klassische Verwechslung)
-r rekursiv ins Verzeichnis; ohne -r scheitert das Kopieren eines Verzeichnisses
-i wählt den Schlüssel; -o reicht ssh-Optionen durch, etwa -o ConnectTimeout=5
-C komprimiert während der Übertragung, lohnt bei Text und Logs, nicht bei bereits gepackten Archiven
-l 8000 begrenzt auf Kbit/s (8000 ≈ 1MB/s), damit die Produktionsbandbreite nicht ausgelastet wird
-p (klein) erhält Zeiten und Rechte — unabhängig von -P, nicht verwechseln
scp kann nicht fortsetzen; ein Abbruch bei mehreren GB startet neu, dafür rsync -P nehmen

scp -P 2222 app.tar.gz deploy@10.0.0.5:/srv/
scp -r -i ~/.ssh/id_deploy ./dist deploy@10.0.0.5:/srv/app/
scp -C -l 8000 backup.tar.gz deploy@10.0.0.5:/backup/
scp -p deploy@10.0.0.5:/var/log/app/error.log ./

sftp: interaktive Übertragung

sftp benutzer@host öffnet eine interaktive Sitzung — „nebenbei ls und übertragen“ ist kontrollierter als blindes scp
-P (groß) Port und -i Schlüsselauswahl, wie bei scp
get REMOTE lädt herunter, put LOKAL lädt hoch; get -r holt ein ganzes Verzeichnis
reget setzt einen Download fort, reput einen Upload — der eigentliche Vorsprung gegenüber scp
-p (bei get/put) erhält Rechte und Zeitstempel
mget / mput nehmen Wildcards, fragen aber standardmäßig pro Datei nach — vorher prompt off
ls / lls zeigen remote und lokal; cd / lcd wechseln remote bzw. lokal (die l-Varianten sind lokal)
!df -h führt ein lokales Shell-Kommando aus — vor großen Downloads den Platz prüfen
bye / exit beendet; ein Heredoc-Skript macht Batch-Übertragungen nicht-interaktiv

sftp -P 2222 -i ~/.ssh/id_deploy deploy@10.0.0.5 <<'EOF'
get -r /srv/app/logs ./
reget backup.tar.gz
put ./dist/app.tar.gz /srv/
EOF

rsync: inkrementeller Abgleich mit Fortsetzen

-a Archivmodus (= -rlptgoD): behält Rechte, Zeiten, Symlinks, Gruppe — der Alltagsstandard
-v zeigt den Fortschritt; -z komprimiert unterwegs, über das Internet Pflicht
-P entspricht --partial --progress: halbe Datei behalten und Fortschritt zeigen — das ist der Fortsetzen-Schalter
--partial behält Übertragenes, der nächste Lauf macht weiter; ohne es startet jeder Lauf neu
--append hängt nur an, gut für reine Append-Logs, falsch für sich ändernde Dateien
--delete spiegelt das Ziel zur Quelle (entfernt Zusätzliches) — vorher mit -n proben
-n und --dry-run sind gleich: zeigen nur, was passieren würde, ändern nichts
-e 'ssh -p 2222' wählt Remote-Shell und Port; rsync selbst hat keine Portoption
--exclude schließt ein Muster aus, --exclude-from liest eine Liste aus einer Datei
--bwlimit=2000 drosselt in KB/s; --checksum vergleicht Inhalte statt Größe und Zeit
--info=progress2 zeigt eine Gesamtfortschrittszeile, bei großen Bäumen besser als -P pro Datei
Die größte Falle: ein abschließendes / in der Quelle kopiert den INHALT, ohne / das Verzeichnis selbst

rsync -avzP ./dist/ deploy@10.0.0.5:/srv/app/dist/
rsync -avzP -e 'ssh -p 2222' deploy@10.0.0.5:/var/log/app/ ./logs/
rsync -avn --delete /srv/data/ /backup/data/          # Probelauf, erst genau ansehen, dann echt ausführen
rsync -avz --bwlimit=2000 --exclude='*.tmp' ./ /mnt/nas/app/

wget: Download und Fortsetzen

wget URL lädt ins aktuelle Verzeichnis; -O benennt die Ausgabe, -O - schreibt nach stdout
-c setzt fort (curl braucht -C -); nach einem Abbruch dasselbe Kommando erneut ausführen
-q Stillmodus; --progress=dot:giga zeigt kompakten Fortschritt
--limit-rate=1m drosselt, mit k/m-Suffix; --tries=3 setzt die Versuche
-T 30 Verbindungs-Timeout in Sekunden; --timeout ist die lange Form
-r spiegelt eine Site, -np verhindert den Aufstieg in Elternverzeichnisse (sonst die ganze Domain)
--no-check-certificate überspringt die Zertifikatsprüfung, nur zur internen Diagnose, nie im Skript
Für eigene Header, Cookies oder Sessions ist curl das bessere Werkzeug

wget -c https://example.com/big.iso
wget -O - https://example.com/install.sh | head -40   # erst Inhalt ansehen, dann über die Ausführung entscheiden
wget --limit-rate=1m --tries=3 -c -O app.tar.gz https://example.com/app.tar.gz

🔐 Rechte und Benutzer (6)

Numerische Bits, Spezialbits, sudo — falsche Rechte zeigen sich als „Passwort stimmt, Zugriff verweigert“ oder „Dienst startet nicht“

chmod: numerische und symbolische Rechte

Numerisch: 4 lesen / 2 schreiben / 1 ausführen, die Stellen sind Eigentümer / Gruppe / andere
755 für Verzeichnisse (rwxr-xr-x); 644 für Dateien (rw-r--r--)
600 nur Eigentümer lesen und schreiben, für private Schlüssel Pflicht
Symbolisch: u/g/o/a für wen, +/-/= für was, X setzt Ausführbar nur bei Verzeichnissen
-R rekursiv; für Verzeichnisse besser a+X statt a+x, damit normale Dateien nicht ausführbar werden
go-w nimmt Gruppe und anderen das Schreibrecht, im selben Durchgang wie a+X
Nach einer rekursiven Änderung eine Datei und ein Verzeichnis mit ls -l prüfen, falsche Rechte kommen meist vom Zuviel

chmod 600 ~/.ssh/id_rsa
chmod -R a+X,go-w /srv/app/public
ls -l /srv/app/public | head    # nach rekursiver Änderung stichprobenartig prüfen

chown: Eigentümer und Gruppe ändern

chown benutzer:gruppe pfad, als Trenner geht auch ein Punkt
Nur die Gruppe ändern mit chown :gruppe oder chgrp
-R rekursiv; mit -h wird der Symlink selbst geändert, nicht sein Ziel
--reference=DATEI übernimmt Eigentümer und Rechte einer vorhandenen Datei
Nur root ändert Eigentümer; Benutzer nur die Gruppe eigener Dateien (und nur in einer eigenen Gruppe)
Bei Permission denied auf einem Container-Volume die uid des Host-Verzeichnisses mit dem Container-Benutzer vergleichen

chown -R www-data:www-data /var/www/app
chown --reference=/srv/app/a /srv/app/b

SUID / SGID / Sticky: die drei Spezialbits

4000 SUID: läuft als Eigentümer der Datei (so arbeitet /usr/bin/passwd)
2000 SGID: läuft als Gruppe; bei Verzeichnissen erben neue Dateien diese Gruppe
1000 Sticky: im Verzeichnis darf nur der Eigentümer löschen (/tmp arbeitet so)
Ein s oder t in ls zeigt das gesetzte Bit; großes S/T heißt gesetzt, aber ohne x
-perm -4000 bedeutet „Rechtebits enthalten SUID“; -perm ist die Rechteprüfung von find
-xdev bleibt auf einem Dateisystem — sonst läuft find durch NFS und Container-Mounts und kriecht
-ls gibt Treffer als ls -dils aus, mit Größe und Zeit zusammen
-type f beschränkt auf reguläre Dateien, ohne Verzeichnisse und Symlinks
Für die Rechteausweitung zuerst SUID suchen: find / -xdev -perm -4000 -type f

# Audit: alle SUID-Programme auflisten
find / -xdev -perm -4000 -type f -ls 2>/dev/null

# gemeinsames Verzeichnis: Zusammenarbeit in der Gruppe + Löschen nur durch den Eigentümer
chmod 2770 /srv/share

umask: Standardrechte neuer Dateien

umask ist die Menge der zu ENTFERNENDEN Bits, nicht der Modus selbst
022 ergibt Verzeichnisse 755 und Dateien 644 (Dateien bekommen nie standardmäßig x)
077 ergibt Verzeichnisse 700 und Dateien 600 — richtig für Schlüsselmaterial
-S zeigt den resultierenden Standardmodus symbolisch (rwxr-xr-x), lesbarer als Ziffern
Temporär: umask 077 gilt nur in der aktuellen Shell
Dauerhaft: in /etc/profile oder ~/.bashrc eintragen
Wenn neue Schlüssel weltlesbar sind, zuerst umask prüfen

umask            # aktuellen Wert anzeigen
umask 077        # gilt für die aktuelle Sitzung
umask -S         # tatsächliche Standardrechte in symbolischer Form anzeigen

sudo / su: richtig erhöhen

sudo -i öffnet eine frische root-Login-Shell (lädt die Umgebung von root)
sudo -s root-Shell mit der eigenen Umgebung und dem eigenen PATH
sudo -u BENUTZER führt als anderer Benutzer aus, oft beim Testen von Diensten
sudo -l listet die erlaubten Kommandos — erste Anlaufstelle bei „warum darf ich das nicht“
su - wechselt zu root mit dessen Umgebung; ohne Bindestrich bleibt die eigene, der PATH kann falsch sein
sudoers mit visudo bearbeiten; -c prüft die Syntax ohne Speichern — nach jeder Änderung laufen lassen
/etc/sudoers von Hand editieren kann aussperren — der klassische selbst gebaute Ausfall

sudo -l
sudo -u www-data /usr/bin/env      # Umgebung des Dienstkontos anzeigen
visudo -c                          # nur Syntaxprüfung

useradd / usermod / id

useradd -m legt ein Home-Verzeichnis an; -s setzt die Shell; -G ergänzt Zusatzgruppen
Ohne -m fehlt das Home-Verzeichnis und viele Werkzeuge scheitern unverständlich
usermod -aG GRUPPE BENUTZER — ohne -a fliegt der Benutzer aus allen anderen Gruppen (-a ist append)
-G nimmt die Gruppenliste und lässt sich als -aG kombinieren
id BENUTZER zeigt uid/gid und alle Gruppen; groups BENUTZER nur die Gruppen
userdel -r entfernt auch das Home-Verzeichnis; ohne -r bleiben verwaiste Dateien
Gruppenänderungen gelten nicht für laufende Sitzungen — neu einloggen

useradd -m -s /bin/bash deploy
usermod -aG docker deploy
id deploy

⚙️ Prozesse und Ressourcen (8)

Wer verbraucht CPU, wer hält die Datei, wie beendet man sauber — der typische Fehler hier ist ein zu frühes kill -9

ps: zwei Stile, nicht mischen

ps aux ist BSD-Stil: %CPU, %MEM, STAT, COMMAND — die Alltagswahl
ps -ef ist System-V-Stil: UID, PID, PPID, C, STIME, CMD — zeigt die Eltern-PID
-e wählt alle Prozesse (System-V-Stil), -o bestimmt die Ausgabespalten
ps -aux läuft zwar, bedeutet aber etwas anderes; nicht zur Gewohnheit machen
--sort=-%cpu sortiert nach CPU absteigend (Minus heißt absteigend), Kommas verketten Schlüssel
--sort=-rss sortiert nach realem Speicher; in STAT ist Z Zombie, D uninterruptible, R running
RSS ist der echte Verbrauch, VSZ der Adressraum und weitgehend ohne Aussage

ps aux --sort=-%cpu | head
ps -eo pid,ppid,rss,pcpu,cmd --sort=-rss | head

top / htop: die Tasten zählen mehr als die Optionen

In top sortiert P nach CPU, M nach Speicher, T nach kumulierter Zeit
1 klappt die Details je Kern auf — trennt einen vollen Kern von allgemeiner Last
c zeigt die vollständige Kommandozeile statt der gekürzten
H schaltet Threads um; u filtert nach Benutzer; k beendet
-b ist Batchmodus und -n1 druckt einen einzelnen Snapshot (-n ist die Anzahl) — gut für Skripte
-p beschränkt auf bestimmte PIDs, kommagetrennt; pgrep -d, verkettet mehrere PIDs mit Komma
Die Load average ist der Mittelwert über 1/5/15 Minuten; über der Kernzahl heißt es wartet

top -b -n1 | head -15          # einmal einen Snapshot aufnehmen
top -p $(pgrep -d, nginx)      # nur die angegebenen Prozesse anzeigen

kill: erst bitten, dann erzwingen

kill sendet standardmäßig TERM(15) — der Prozess kann aufräumen, schreiben und sauber beenden
-15 entspricht -TERM, die benannte Form liest sich besser und ist der erste Griff
-9 ist KILL: der Kernel entzieht ihn sofort, ohne Aufräumen — Datenbanken und Queues können beschädigt werden
-1 oder -HUP wird meist zum Neuladen der Konfiguration genutzt (nginx, sshd)
-0 sendet nichts und prüft nur die Existenz; der Exit-Code ist die Antwort
kill nimmt PIDs, keine Namen; für Namen pkill oder killall verwenden
pkill -HUP nginx signalisiert alle nginx-Prozesse per Name
Immer TERM, ein paar Sekunden warten, dann KILL — diese Reihenfolge steht in jeder Betriebsrichtlinie

kill -TERM 1234 && sleep 3
kill -0 1234 && kill -9 1234
pkill -HUP nginx               # Konfiguration neu laden

nohup / setsid: das Terminal überleben

& legt nur in den Hintergrund; beim Schließen des Terminals kommt SIGHUP und der Prozess stirbt
nohup ignoriert SIGHUP und schreibt standardmäßig nach nohup.out (besser explizit umleiten)
2>&1 führt stderr mit stdout zusammen, sonst verschwinden Fehler irgendwo
< /dev/null legt stdin auf das Null-Device, damit der Job nicht auf Terminal-Eingabe blockiert
setsid löst den Prozess aus der Sitzung — gründlicher als nohup
disown entfernt einen laufenden Hintergrundjob aus der Job-Tabelle der Shell
Für dauerhafte Dienste systemd nehmen; nohup dient nur der kurzen Prüfung

nohup ./run.sh > /var/log/run.log 2>&1 &
setsid ./worker.sh < /dev/null > /var/log/worker.log 2>&1 &

lsof: wer hält diese Datei oder diesen Port

lsof -i :8080 zeigt, wer Port 8080 hält (-i filtert nach Netzwerkadresse)
-p PID listet alle Dateien eines Prozesses
-u BENUTZER filtert nach Benutzer; -n überspringt die Namensauflösung, schneller und ohne DNS-Hänger
+D /pfad geht ein Verzeichnis rekursiv durch (bei großen Bäumen langsam)
+L1 listet Dateien mit Linkzahl unter 1 — gelöscht, aber offen, die übliche Ursache belegten Speichers
sort -k7 -n sortiert nach Spalte 7 (SIZE/OFF) numerisch
Ohne lsof helfen fuser -v oder ein Blick in /proc/PID/fd

lsof -i :8080
lsof -n +L1 | sort -k7 -n | tail    # gelöschte, noch belegte Dateien nach Größe anzeigen

nice / renice / taskset: CPU abgeben

nice reicht von -20 (höchste Priorität) bis 19 (niedrigste); normale Benutzer dürfen nur erhöhen
nice -n 19 KOMMANDO startet mit niedriger Priorität — richtig für Backups und Kompression
-n trägt den nice-Wert selbst, keine Anzahl
renice -n 10 -p PID ändert einen laufenden Prozess; -p nennt die PID, -u arbeitet pro Benutzer
taskset -c 0-3 bindet CPUs: -c nimmt die CPU-Liste und mindert Streit zwischen Instanzen
In taskset -cp 0-3 1234 bedeutet -p „auf bestehenden Prozess wirken“, die PID folgt danach
Niedrigere Priorität schafft keine Kapazität — sie schützt nur die wichtige Arbeit
Im Beispiel tar -czf: -c erzeugt, -z gzip, -f benennt das Archiv (siehe tar)

nice -n 19 tar -czf backup.tar.gz /srv/data
renice -n 10 -p 1234
taskset -cp 0-3 1234

watch: Änderung verfolgen statt Snapshot

watch wiederholt das Kommando standardmäßig alle 2 Sekunden und leert den Bildschirm — man sieht Änderung, nicht Snapshot
-n 1 aktualisiert jede Sekunde; -n nimmt die Sekunden (0.5 ist erlaubt)
-d hebt hervor, was sich zwischen zwei Läufen geändert hat
-d=cumulative behält die Markierung an allem, was je geändert war — gut bei sporadischen Ausschlägen
-t blendet die Kopfzeile aus (Kommando und Intervall), das Bild wird ruhiger
Kommando in einfache Anführungszeichen setzen, sonst zerlegt die Shell die Pipe und watch sieht nur den ersten Teil
Nicht für hochfrequente Kennzahlen (Verbindungen, QPS) nutzen; vmstat 1 oder sar zeigt, was watch verpasst
In den Beispielen fasst ss -s Verbindungen zusammen und wc -l zählt Zeilen

watch -n 1 -d 'ss -tn state established | wc -l'
watch -d=cumulative -t 'ss -s'

screen / tmux: einen Verbindungsabbruch überstehen

screen -S deploy öffnet eine benannte Sitzung; Terminal schließen oder Netzverlust lässt sie laufen
-list listet Sitzungen (auch -ls) — zuerst nach einer alten Attached-Sitzung sehen
-r deploy hängt wieder an; bei Attached hält sie jemand anders — mit -d -r übernehmen
Ctrl-a d löst in screen, Ctrl-a " listet Fenster, Ctrl-a k beendet das aktuelle
tmux new -s deploy erzeugt eine, tmux attach -t deploy hängt wieder an (-t benennt sie)
In tmux löst Ctrl-b d, Ctrl-b [ geht in den Copy-Modus zum Zurückblättern
Vor Migrationen, Backups oder großen Transfers eine Sitzung starten — ein Verbindungsabbruch stoppt den Job nicht, er verbirgt ihn nur

screen -S deploy
# danach mit Ctrl-a d abkoppeln
screen -list
screen -r deploy
tmux new -s deploy

🌐 Netzwerk und Ports (8)

Wenn ein Dienst nicht antwortet: erst trennen zwischen „lauscht nicht“, „nicht freigegeben“ und „nicht erreichbar“ — falsche Reihenfolge kostet viel Zeit

ss: wer lauscht wo

-t TCP; -u UDP; -l nur Lauschende; -n numerisch (keine Namensauflösung, schneller); -p Prozess anzeigen
-tulnp ist die Alltagskombination — fünf Buchstaben, fünf Aufgaben
-tnp ohne das l zeigt auch den Prozess etablierter Verbindungen, die übliche Zählprüfung
-a nimmt etablierte Verbindungen mit; -s Zusammenfassung — erster Blick bei explodierenden Verbindungszahlen
state established filtert nach Zustand, genauer als ein grep auf ESTAB (das ist nur die Spaltenabkürzung)
netstat ist veraltet und auf modernen Systemen oft nicht installiert; ss verwenden
127.0.0.1 oder 0.0.0.0 macht einen Unterschied: Ersteres ist nur lokal, die häufigste Ursache für „Port offen, aber keine Verbindung“

ss -tulnp
ss -tnp state established | head
ss -s

curl: das Taschenmesser für Schnittstellen

-s still: keine Fortschrittsanzeige, keine Fehlertexte — in Skripten fast immer gewünscht
-S wirkt mit -s zusammen und zeigt Fehler wieder an — -sS heißt also „still, aber bei Fehler laut“
-i nimmt Antwort-Header mit; -I nur Header (sendet HEAD, kein Body)
-v zeichnet Handshake und Anfrage vollständig auf — zeigt genau, an welchem Schritt es hängt
-L folgt Weiterleitungen; bei einer 301/302-Kette unverzichtbar
-X setzt die Methode; -d sendet einen Body (Standard POST); -H ergänzt einen Header
-o /dev/null verwirft den Body und ergibt mit -w nur Status und Zeit
-w '%{http_code} %{time_total}' eigene Ausgabe, schneller als Schätzen
-m 10 begrenzt die ganze Anfrage auf 10s; --connect-timeout 3 nur die Verbindungsphase
--resolve host:443:IP bindet eine IP und umgeht DNS — zeigt, ob DNS das Problem ist
-x http://proxy:8080 geht über einen Proxy; --noproxy '*' erzwingt direkt (lokal fast immer nötig)
-k überspringt die Zertifikatsprüfung; nur zur Diagnose, nie im Skript
In -w ist \n ein Zeilenumbruch, ohne ihn landet alles in einer Zeile

curl -sS -o /dev/null -w '%{http_code} %{time_total}s\n' https://example.com/
curl -m 10 --connect-timeout 3 -sS -o /dev/null \
  -w 'dns:%{time_namelookup} conn:%{time_connect} ttfb:%{time_starttransfer} total:%{time_total}\n' \
  https://example.com/api
curl -iL --resolve example.com:443:1.2.3.4 https://example.com/
curl -v -X POST -H 'Content-Type: application/json' -d '{"a":1}' http://127.0.0.1:8080/api

Port prüfen: nc / telnet / /dev/tcp

nc -zv HOST PORT — -z prüft ohne Daten zu senden, -v zeigt das Ergebnis, die schnellste Variante
-w3 setzt 3 Sekunden Timeout, damit das Kommando nicht hängt (ohne Angabe dauert es lange)
telnet HOST PORT existiert auf alten Systemen; „Connected“ heißt erreichbar
Bash-eigenes /dev/tcp braucht nichts installiert: echo > /dev/tcp/HOST/PORT
„refused“ heißt Host erreichbar, aber niemand lauscht; ein Timeout heißt Firewall oder Security Group verwirft — völlig verschiedene Bedeutungen
Immer schichtenweise eingrenzen: lokal ss, dann curl 127.0.0.1, dann nc aus demselben Subnetz, dann von außen

nc -zv -w3 10.0.0.5 5432
echo > /dev/tcp/10.0.0.5/5432 && echo open || echo closed

ip: Adressen, Routen, Nachbarn

ip a zeigt alle Interface-Adressen (das alte ifconfig); -4 nur IPv4, -6 nur IPv6
ip r druckt die Routing-Tabelle; die default-Zeile ist das Gateway
ip neigh zeigt die ARP-Tabelle — bei Problemen im selben Subnetz hier nachsehen
ip -s link liefert Paket- und Fehlerzähler (-s ist statistics); errors/dropped ungleich 0 heißt Link-Problem
show eth0 beschränkt die Ausgabe auf ein Interface, virtuelle Interfaces übertönen sonst alles
add / del ändern Adressen temporär, nach dem Reboot sind sie weg
Dauerhaft je Distribution: netplan oder /etc/network/interfaces bei Debian, nmcli bei RHEL

ip -4 a
ip r
ip -s link show eth0

dig: was DNS wirklich liefert

+short gibt nur die Antwort aus — skriptfreundlich
+A / +AAAA / +MX wählen den Record-Typ (Standard ist A)
@8.8.8.8 fragt einen bestimmten Resolver, um lokal mit öffentlich zu vergleichen
+trace läuft von der Wurzel abwärts und findet die falsche Delegation
Bei Fehlschlag zuerst status prüfen: NXDOMAIN heißt kein Record, SERVFAIL heißt Server defekt
Die TTL bestimmt, wie lange eine Änderung braucht; die Rest-TTL im ANSWER SECTION zeigt, wie alt der Cache ist
dig deckt nur DNS ab. Klappt die Auflösung, aber der Verkehr nicht, liegt es an Routing und Ports

dig +short example.com
dig @8.8.8.8 example.com A
dig +trace example.com

tcpdump: sehen, wer nicht mehr ACKt

-i any lauscht auf allen Interfaces (oder eines benennen); -n überspringt DNS- und Portnamen (schneller, DNS kann nicht bremsen)
-nn heißt numerische IPs und Ports, beim Debuggen klarer als -n allein
port 443 filtert nach Port; host 1.2.3.4 nach Host; beides mit and / or kombinierbar
-c 100 stoppt nach 100 Paketen statt das Terminal zu fluten
-w file.pcap speichert für Wireshark, viel besser als im Terminal zu lesen
-A gibt die Nutzlast als ASCII aus, -X als Hex plus ASCII (nur bei Klartextprotokollen sinnvoll)
tcp[tcpflags] & tcp-rst != 0 fängt nur RST — zeigt, wer zuerst getrennt hat
Braucht root; in Produktion immer -c und einen Filter setzen

tcpdump -i any -nn -c 50 'port 443 and host 1.2.3.4'
tcpdump -i eth0 -nn -w /tmp/cap.pcap 'tcp[tcpflags] & tcp-rst != 0'

openssl s_client: Ablauf und Kette

-connect HOST:443 prüft das Peer-Zertifikat auf TLS-Ebene, HTTP bleibt außen vor
-servername HOST ist Pflicht (SNI), sonst liefert eine Multi-Domain-IP das falsche Zertifikat
-showcerts druckt die ganze Kette und zeigt ein fehlendes Intermediate
-noout unterdrückt das Rohzertifikat und gibt nur das Ausgewählte aus
-dates zeigt notBefore / notAfter — der erste Schritt jeder Ablaufprüfung
-subject zeigt, für wen das Zertifikat ausgestellt ist; -ext subjectAltName listet die abgedeckten Namen
-checkend 2592000 prüft, ob es in 30 Tagen (Sekunden) abläuft; der Exit-Code ist monitoringtauglich
</dev/null lässt s_client nach dem Lesen beenden, sonst wartet es auf Eingabe

openssl s_client -connect www.icetools.cn:443 -servername www.icetools.cn </dev/null 2>/dev/null \
  | openssl x509 -noout -dates -subject
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null | head -30
openssl s_client -connect example.com:443 -servername example.com -checkend 2592000 </dev/null >/dev/null \
  && echo '30 天内不过期'

Firewall: erst prüfen, ob sie blockt

firewall-cmd --list-all zeigt, was die aktive Zone erlaubt (RHEL-Familie)
--add-port=8080/tcp fügt eine Regel hinzu; nur --permanent überlebt einen Reload
--reload wendet permanente Regeln an — nach dem Hinzufügen Pflicht, sonst erreichen sie den Laufzeitzustand nie
ufw status verbose zeigt die Regeln (Ubuntu)
iptables -L listet die Filtertabelle; -n hält Namen numerisch, -v zeigt Paketzähler und damit, ob eine Regel greift
--line-numbers zeigt Regelnummern, die man für iptables -D INPUT 3 braucht
-I INPUT 1 fügt oben ein, -A hängt an — die Reihenfolge entscheidet
Hilft es nach der Regeländerung nicht, zuerst die Security Group des Cloud-Anbieters prüfen, sie liegt außerhalb dieser Maschine

firewall-cmd --list-all
firewall-cmd --add-port=8080/tcp --permanent && firewall-cmd --reload
iptables -L INPUT -n -v --line-numbers

💾 Platten und Dateisysteme (5)

„Platte voll“ hat vier verschiedene Ursachen: viele Dateien, eine große Datei, erschöpfte Inodes, gelöschte aber offene Dateien

Platte voll: Ablauf in vier Schritten

Schritt 1: df -h zeigt den vollen Mountpoint — den Dienst noch nicht neu starten
Schritt 2: df -i für Inodes. Erschöpfte Inodes melden „no space left on device“, während df -h noch Platz zeigt
Schritt 3: du -h --max-depth=1 steigt Ebene für Ebene zum großen Verzeichnis ab (--max-depth ist die Ebene)
Schritt 4: weichen beide Zahlen ab, findet lsof +L1 gelöschte, aber offene Dateien; erst Neustart oder kill gibt sie frei
Die awk-Zeile summiert Spalte 7 (Bytes) und rechnet in MB um — schneller als Schätzen
Log-Wachstum zeigt sich meist schon in Schritt 1 — vor dem Löschen lesen, was da spammt

df -h; df -i
du -h --max-depth=1 / | sort -h | tail
lsof +L1 2>/dev/null | awk '{s+=$7} END {print s/1024/1024" MB held"}'

lsblk: erst den Aufbau verstehen

Standard ist eine Baumansicht, die LVM, Partitionen und Mountpoints verschachtelt zeigt
-f zeigt Dateisystemtyp, UUID und Mountpoint — vor dem Bearbeiten der fstab prüfen
-p druckt vollständige Gerätepfade, damit /dev/sda und /dev/sda1 nicht verwechselt werden
-d blendet Kindgeräte aus und zeigt nur ganze Platten
-o wählt die Spalten; NAME,SIZE,FSTYPE,MOUNTPOINT,UUID ist der nützliche Satz
Nach dem Vergrößern: zeigt lsblk die neue Größe, df aber nicht, wurde das Dateisystem noch nicht erweitert

lsblk -f
lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT,UUID

Vergrößern: Dateisystem mitziehen

Eine Cloud-Platte zu vergrößern sind zwei Schritte: Partition, dann Dateisystem — fehlt einer, ändert sich df nicht
growpart /dev/vda 1 erweitert die Partition (cloud-utils-growpart), das zweite Argument ist die Partitionsnummer
xfs_growfs / erweitert XFS (nur größer, nicht kleiner)
resize2fs /dev/vda1 erweitert ext4 (auch kleiner, aber erst nach umount und Prüfung)
LVM braucht zusätzlich lvextend, danach das Resize des Dateisystems
Mit df -h bestätigen, dass die Kapazität wirklich gewachsen ist (-h ist human-readable)
Vorher einen Snapshot ziehen — Stromausfall mitten im Resize kann Daten kosten

growpart /dev/vda 1
xfs_growfs /            # XFS
resize2fs /dev/vda1     # ext4
df -h /

mount / umount / fstab

Häufige -o Optionen: ro nur lesen, remount neu einhängen, noatime gegen Write-Amplification
remount ist das Arbeitstier: Mount-Optionen ändern ohne Aushängen, nichts fällt aus
Meldet umount „device is busy“, mit lsof +D oder fuser -vm den Halter suchen
In der fstab besser UUID als /dev/sdb, Gerätenamen können sich ändern
Feld 4 ist meist defaults,noatime; Felder 5 und 6 sind dump und fsck-Reihenfolge, für Datenplatten 0 0
Nach jeder fstab-Änderung mount -a prüfen; eine falsche Zeile kann den nächsten Boot verhindern
mount -a hängt alles aus der fstab ein, was noch nicht hängt, und nennt bei Fehler die betroffene Zeile

mount -o remount,rw /
fuser -vm /mnt/data
mount -a                      # fstab-Eintrag prüfen

Inodes erschöpft: auch kleine Dateien scheitern

Jede Datei belegt einen Inode, die Gesamtzahl steht beim Formatieren fest
Symptom: df -h zeigt Platz, Schreiben scheitert mit „No space left on device“
df -i zeigt IUse%, nahe 100% bestätigt es
-xdev bleibt auf einem Dateisystem, damit die Zählung nicht in andere Mounts läuft
-type f zählt nur Dateien — Verzeichnisse belegen auch Inodes, die Dateiverteilung ist aber die nützliche Sicht
Das sed schneidet alles nach dem letzten Slash weg und lässt das Verzeichnis übrig; sort | uniq -c zählt Dateien je Verzeichnis
Übliche Ursachen: Session-Dateien, Upload-Tempfiles, Mail-Spools, Cache-Verzeichnisse mit Hunderttausenden kleiner Dateien
Sauber löst nur ein Neuformatieren mit größerem -i-Verhältnis; kurzfristig kleine Dateien löschen
Das abschließende sort -rn heißt numerisch absteigend: -r umgekehrt, -n numerisch

df -i
find /var -xdev -type f | sed 's#/[^/]*$##' | sort | uniq -c | sort -rn | head

⏰ Cronjobs und Zeit (4)

Cronjobs scheitern meist an PATH und Output-Umleitung; eine falsche Uhrzeit zieht Zertifikate, JWTs und Logs mit

crontab: fünf Zeitfelder

crontab -l listet die Jobs des Benutzers; -e bearbeitet; -r löscht alle ohne zweite Rückfrage
-u BENUTZER arbeitet auf fremder crontab (als root ausführen)
Die fünf Felder sind Minute Stunde Tag Monat Wochentag: 0 3 * * * heißt täglich 03:00
*/5 alle 5 Minuten; 1-5 Montag bis Freitag; 0,12 nur um 00:00 und 12:00
Die cron-Umgebung ist nicht die Login-Shell, der PATH ist minimal — im Skript immer absolute Pfade
cron liest ~/.bashrc nicht; Variablen oben in der crontab setzen oder im Skript sourcen
Ausgabe umleiten: >> /var/log/x.log 2>&1, sonst gehen Fehler per Mail weg und bleiben unsichtbar
Wenn nichts lief: journalctl -u cron (Debian) oder /var/log/cron (RHEL)
In den Beispielen ist curl -sS still, meldet aber Fehler (-s still, -S zeigt Fehler), -o /dev/null verwirft den Body

crontab -e
crontab -l
# täglich um 3:30 sichern, Ausgabe ins Log
30 3 * * * /srv/app/backup.sh >> /var/log/backup.log 2>&1
# alle 5 Minuten einen Healthcheck ausführen
*/5 * * * * /usr/bin/curl -sS -o /dev/null http://127.0.0.1:8080/health

systemd-Timer: leichter zu debuggen als cron

Echte Vorteile: Logs landen im journalctl, der nächste Lauf ist sichtbar, verpasste Läufe können nachholen
OnCalendar nimmt Kalendersyntax: *-*-* 03:30:00 täglich 03:30; OnUnitActiveSec=5min alle 5 Minuten
Persistent=true holt einen verpassten Lauf nach dem Boot nach — cron kann das nicht
Unit= benennt den auszuführenden Service, ohne Angabe gilt die gleichnamige .service
systemctl list-timers zeigt alle Timer und die nächste Auslösung
enable --now heißt „beim Boot starten und jetzt laufen“; enable allein löst noch nichts aus
Zum Testen die .service starten, nicht den .timer — der Timer-Start feuert den Job nicht
Die letzte Zeile: journalctl -u wählt die Unit, -n 30 nimmt die letzten 30 Zeilen, --no-pager vermeidet Paging

# /etc/systemd/system/backup.timer
[Unit]
Description=Daily backup

[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true

[Install]
WantedBy=timers.target

# aktivieren und prüfen
systemctl list-timers
systemctl enable --now backup.timer
journalctl -u backup -n 30 --no-pager

date / timedatectl: Uhrzeit und Zeitzone

date zeigt Uhrzeit und Zeitzone; +%F ist 2026-10-29, +%T die Uhrzeit, '+%F %T' das übliche Paar
-d '-1 hour' rechnet eine Stunde zurück, -d 'next monday' geht auch — jedes Zeitfenster im Skript nutzt -d
-s setzt die Uhr direkt; nie in Produktion, es zerwürfelt Log-Reihenfolge und Zertifikatsprüfungen
timedatectl zeigt Zeitzone und NTP-Synchronisation, vollständiger als date
set-timezone Asia/Shanghai behebt das häufigste lokale Problem: noch auf UTC
set-ntp true aktiviert die automatische Synchronisation; Container erben die Host-Uhr
Eine falsche Uhr zeigt sich konkret: „not yet valid“-Zertifikate, sofort abgelaufene JWTs, unpassende Log-Zeiten, Jobs zur falschen Stunde

date '+%F %T'
date -d '-1 hour' '+%F %T'
timedatectl
timedatectl set-timezone Asia/Shanghai

chronyc / ntpq: läuft die Synchronisation

chronyc tracking zeigt Offset und Stratum; die System-time-Zeile sagt, wie weit die Uhr daneben liegt
Ein Offset über einigen hundert Millisekunden heißt Ärger: Quelle unerreichbar, UDP 123 blockiert, VM-Uhr driftet
chronyc sources -v listet jede Quelle, -v zeigt Details (Delay, Jitter, ausgewählt oder nicht)
Ein führendes ^* markiert die genutzte Quelle; ^? heißt unerreichbar
chronyc makestep stellt die Uhr sofort — manuell nur bei großem Offset, wird protokolliert
ntpq -p ist das Gegenstück für alte ntpd-Systeme, -p druckt die Peer-Liste
Auf Cloud-Hosts besser das interne NTP des Anbieters nutzen; öffentliches NTP ist oft gedrosselt oder verweigert

chronyc tracking
chronyc sources -v
chronyc makestep
ntpq -p          # ältere Systeme (ntpd)

📋 Logs und Fehlersuche (5)

Die Reihenfolge zählt mehr als das Kommando: erst das Zeitfenster eingrenzen, dann per Stichwort verengen, dann den Kontext lesen

journalctl: das zentrale systemd-Log

-u UNIT filtert auf einen Dienst
-f folgt; --since '10 min ago' oder --since '2026-10-29 09:00' grenzt das Fenster ein
-p err zeigt error und höher; die Stufen reichen von emerg bis debug
-n 100 die letzten 100 Zeilen; -r umgekehrt (neueste zuerst)
--no-pager vermeidet den Pager, hilft beim Umleiten und in Pipes
-o short-iso enthält den Zeitzonen-Offset — über Regionen hinweg unverzichtbar
--disk-usage zeigt den Platzbedarf des Journals; --vacuum-time=7d behält nur 7 Tage
--vacuum-size=500M kürzt nach Volumen — eines von beiden wählen

journalctl -u nginx -p err --since '30 min ago' --no-pager
journalctl -u app -f -o short-iso
journalctl --disk-usage

tail: -f gegen -F

-f folgt dem Dateideskriptor; nach einer Rotation liest es die alte Datei weiter und das Log wirkt eingefroren
-F folgt dem Namen und öffnet nach der Rotation neu — beim längeren Beobachten immer diese Option
-n 100 zeigt zuerst die letzten 100 Zeilen; -n0 zeigt keine Historie, nur Neues
-Fn0 ist einfach -F plus -n0, das übliche Paar für einen Live-Stream
--pid=PID beendet das Folgen, wenn der Prozess endet
grep --line-buffered lässt grep pro Zeile ausgeben; ohne das stockt die Pipe und man sieht nichts
Zusammen: tail -Fn0 app.log | grep --line-buffered -i error
Mehrere Dateien: tail -F a.log b.log setzt ==> als Trenner; nicht nur eine beobachten

tail -F /var/log/app/error.log
tail -Fn0 /var/log/nginx/access.log | grep --line-buffered -i 500

dmesg: was der Kernel gesehen hat

OOM-Kills, Platten-IO-Fehler, Link-down und Treiberfehler stehen hier
-T druckt lesbare Zeitstempel; -w folgt mit
-l err,warn filtert nach Stufe (-l nimmt eine kommagetrennte Liste)
Der Ringpuffer wird überschrieben — früh mitschneiden; journalctl -k ist die persistente Sicht derselben Daten
„Out of memory: Killed process“ heißt OOM-Killer, kein Absturz
Mit grep -iE: -i ignoriert Groß-/Kleinschreibung, -E aktiviert den erweiterten Regex (damit | wirkt, siehe grep)

dmesg -T -l err,warn | tail -30
dmesg -T | grep -iE 'oom|killed process'

OOM: warum ein Prozess plötzlich weg ist

Bestätigen mit dmesg -T | grep -i -A2 'killed process' — -A2 nimmt die zwei Folgezeilen mit dem Speicherprofil des Opfers
journalctl -k enthält dasselbe Ereignis plus den Speicherzustand von damals
status=9/KILL oder signal=KILL in systemctl status bedeutet fast immer den OOM-Killer
--no-pager hält den Pager heraus, wichtig in Skripten und Screenshots
Das ist kein Absturz: das Anwendungslog endet einfach, ohne Stacktrace
Ursache suchen: free -h für verfügbaren Speicher (-h human readable), dann Prozesse nach RSS sortieren
In Containern zusätzlich das cgroup-Limit unter /sys/fs/cgroup/memory.max prüfen

dmesg -T | grep -i -A2 'killed process'
systemctl status app --no-pager | head -20
free -h

logrotate: Logs dürfen die Platte nicht füllen

Konfigurationen liegen in /etc/logrotate.d/, eine Datei pro Dienst
daily / weekly für den Zeitraum; rotate 7 behält 7 Archive
compress komprimiert alte Logs; delaycompress lässt das neueste unkomprimiert
copytruncate kopiert und leert dann — für Programme, die Dateien nicht neu öffnen können, mit kleinem Verlustfenster
postrotate signalisiert dem Prozess das Neuöffnen, sauberer als copytruncate
-d ist ein Probelauf; nach jeder Änderung erst so laufen lassen
-f erzwingt eine Rotation sofort, um die Konfiguration ohne Wartezeit zu prüfen
missingok schweigt bei fehlender Datei, notifempty überspringt leere Logs — beides gehört in fast jede Konfiguration

# /etc/logrotate.d/app
/var/log/app/*.log {
    daily
    rotate 7
    compress
    delaycompress
    missingok
    notifempty
    postrotate
        systemctl reload app > /dev/null 2>&1 || true
    endscript
}

# erst Probelauf, dann einmal erzwungen zur Kontrolle ausführen
logrotate -d /etc/logrotate.d/app
logrotate -f /etc/logrotate.d/app

🚀 systemd-Dienste (5)

Fast jeder Startfehler ist einer von drei: falscher Pfad, falscher Benutzer, fehlende Abhängigkeit — status sagt welcher

systemctl: die Handvoll für den Alltag

start / stop / restart / reload — reload liest die Konfiguration neu, ohne den Prozess zu starten, nur wenn das Programm es kann
status zeigt die letzten Journal-Zeilen und den Exit-Code; --no-pager hält den Pager fern
enable für Autostart beim Boot; --now startet zusätzlich sofort
disable nimmt den Autostart weg, stoppt aber nicht; dafür separat stop
list-units --failed zeigt, was kaputt ist; --all nimmt inaktive Units mit
is-active / is-enabled liefern Exit-Codes, die direkt in Skripten nutzbar sind
cat zeigt die Unit so, wie systemd sie liest (inklusive Drop-ins) — nach Änderungen prüfen

systemctl status app --no-pager
systemctl enable --now app
systemctl list-units --failed

Eine Unit, die wirklich funktioniert

Ablage in /etc/systemd/system/NAME.service, danach immer daemon-reload
ExecStart braucht einen absoluten Pfad und darf nicht selbst forken (sonst Type=forking)
Restart=on-failure ist die Neustartstrategie, RestartSec=5 verhindert Neustart-Stürme
User / Group bestimmen die Rechte; weg von root heißt meist auch Home und Dateieigentümer anpassen
EnvironmentFile legt Variablen in eine eigene Datei statt sie in ExecStart zu schreiben
WorkingDirectory beeinflusst relative Pfade; viele Programme finden ihre Konfiguration sonst nicht
StandardOutput=journal leitet Logs in den journalctl
After ordnet nur, garantiert nichts; fürs Netz zusätzlich Wants=network-online.target

# /etc/systemd/system/app.service
[Unit]
Description=App
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/srv/app
EnvironmentFile=/etc/app/env
ExecStart=/srv/app/bin/app
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Dienst startet nicht: in dieser Reihenfolge prüfen

1. systemctl status: Active und die letzten Fehlerzeilen prüfen, das klärt die meisten Fälle
2. journalctl -u DIENST -n 50 --no-pager für den vollständigen Trace (-n ist die Zeilenzahl)
3. Prüfen, ob der ExecStart-Pfad existiert und ausführbar ist: ls -l darauf
4. Von Hand als Dienstbenutzer mit sudo -u starten — der echte Fehler erscheint dann meist sofort
5. Policy prüfen: ausearch -m avc -ts recent (-m wählt den avc-Record, -ts die Startzeit)
AppArmor-Verweigerungen landen in dmesg; in Containern zusätzlich docker logs lesen
status=203/EXEC heißt Pfad oder Rechte; status=1 ist der Exit-Code 1 des Programms

systemctl status app --no-pager
journalctl -u app -n 50 --no-pager
sudo -u deploy /srv/app/bin/app     # manuell starten, echte Fehlermeldung ansehen
ausearch -m avc -ts recent           # SELinux-Verweigerungen im Log

Wann daemon-reload Pflicht ist

.service-Datei geändert: daemon-reload ist Pflicht, sonst nutzt systemd die alte Definition
Nur Werte in EnvironmentFile geändert: kein reload nötig, aber restart, damit sie greifen
Neue Unit: erst reload, dann lässt sie sich enablen
Ein laufender Dienst übernimmt neue Konfiguration nicht durch reload — restart nötig
Klassisches Symptom eines vergessenen reload: ExecStart geändert, status zeigt weiter das alte Kommando
cat zeigt die Datei, wie systemd sie sieht, einschließlich Drop-in-Overrides

systemctl daemon-reload && systemctl restart app
systemctl cat app            # prüfen, wie systemd diese Datei sieht

Reihenfolge: After ist nicht Requires

After ordnet nur die Startreihenfolge, garantiert keinen Erfolg
Requires ist eine harte Abhängigkeit: scheitert die andere Unit, scheitert diese mit
Wants ist weich: diese Unit startet trotzdem — der richtige Standard in den meisten Fällen
Für Netzwerk heißt network.target nur, dass der Stack initialisiert ist; Adressen können fehlen
After=network-online.target wartet nur zusammen mit Wants=network-online.target wirklich
„Namensauflösung fehlgeschlagen“ direkt nach dem Boot ist fast immer das
Prüfen mit systemctl list-dependencies DIENST für den Baum

[Unit]
Wants=network-online.target
After=network-online.target

# prüfen
systemctl list-dependencies app

📦 Paketverwaltung (4)

Vor dem Installieren die Quelle prüfen, vor dem Entfernen sehen, was mitgeht — besonders auf Produktionsmaschinen

apt (Debian / Ubuntu)

apt update aktualisiert nur die Indizes und upgradet nichts — das am häufigsten verwechselte Paar
apt upgrade hebt installierte Pakete; full-upgrade darf Pakete entfernen, um Abhängigkeiten zu erfüllen
install -y überspringt die Rückfrage, was Skripte brauchen; ohne -y wartet apt auf Eingabe
apt purge entfernt auch Konfigurationsdateien; reines remove lässt sie liegen
apt list --installed zeigt Vorhandenes; apt list --upgradable filtert auf aktualisierbare
apt-cache show PAKET für Version und Beschreibung
apt-mark hold pinnt eine Version, damit Sammelupgrades sie überspringen

apt update && apt install -y nginx
apt list --upgradable
apt purge nginx-common

dnf / yum (RHEL / CentOS)

dnf ersetzt yum; auf neueren Systemen ist yum meist ein Symlink
install / remove / update weichen von apt ab — nicht vermischen
install -y überspringt Rückfragen, wie bei apt
provides sucht ein Paket anhand von Datei oder Kommando
history listet Transaktionen, history undo last rollt die letzte zurück — rettet nach einem schlechten Upgrade
--enablerepo aktiviert ein Repo für einen Lauf; --nogpgcheck nur wenn es keine Alternative gibt
Module (dnf module list) zählen, wenn mehrere Versionen eines Pakets parallel existieren

dnf install -y nginx
dnf provides /usr/sbin/ss
dnf history undo last

Welches Paket besitzt diese Datei oder dieses Kommando

RHEL: rpm -qf /usr/bin/ss — -q geht in den Abfragemodus, -f sucht nach Datei
Debian: dpkg -S /usr/bin/ss; findet das nichts, erst apt-file update, dann apt-file search
Nur der Kommandoname bekannt: dnf provides '*/ss' oder apt-file search bin/ss
dpkg -L PAKET listet alle Dateien eines Pakets — nützlich vor Konfigurationsänderungen
rpm -qc PAKET listet nur Konfigurationsdateien (-qc ist -q plus -c für config)
Fehlt ein Kommando, erst prüfen, ob das Paket fehlt, statt den PATH zu verdächtigen

rpm -qf /usr/bin/ss
dpkg -S /usr/bin/ss
dpkg -L nginx-common | head -20

Paketcache auf der Platte aufräumen

apt clean entfernt den gesamten .deb-Cache; apt autoclean nur abgelaufene Einträge
dnf clean all leert Cache und Metadaten
apt autoremove entfernt Pakete, die als Abhängigkeit kamen und niemand mehr braucht; --purge auch deren Konfiguration
-y ist bei autoremove weiterhin nötig, sonst bleibt es an der Rückfrage stehen
Vorher mit du -sh prüfen, dass der Platz wirklich belegt ist (-s Summe, -h human readable)
Wer nach apt install im Dockerfile nicht aufräumt, verschenkt pro Layer zig MB

du -sh /var/cache/apt /var/cache/dnf 2>/dev/null
apt clean && apt autoremove --purge -y
dnf clean all

🐳 Docker und Container (8)

Neun von zehn Container-Problemen sind ein Unterschied zwischen vermuteter und tatsächlicher Konfiguration — erst mit inspect die Fakten abgleichen, dann Logs

docker ps: läuft er, und warum startet er neu

-a nimmt beendete Container mit — nur laufende zu zeigen verbirgt den gerade abgestürzten
-q gibt nur Container-IDs aus, praktisch für docker rm oder docker logs
-s zeigt die Größe des beschreibbaren Layers, für die Suche nach einem Container, der die Platte füllt
--format eigene Spalten; '{{.Names}} {{.Status}}' liest sich besser als die Standardtabelle und ist skriptfreundlich
--filter grenzt ein: status=exited für tote, name=web nach Namen
--no-trunc kürzt lange IDs und vollständige Kommandos nicht
-n 5 zeigt nur die 5 zuletzt erstellten (Kurzform von --last)
Up 3 seconds (restarting) in Status heißt Endlos-Neustart — mit docker logs --tail nachsehen

# alle Container (auch beendete)
docker ps -a --format '{{.Names}}  {{.Status}}'

# beendete / ständig neustartende finden
docker ps -a --filter status=exited -n 5

# was die Platte vollgeschrieben hat
docker ps -s --format '{{.Names}}  {{.Size}}'

docker logs: hier liegt die Absturzspur

-f folgt dem Stream, entspricht tail -f
--tail 200 nur die letzten 200 Zeilen — laufende Container haben Zehntausende, sonst scrollt das Wesentliche weg
-t stellt jeder Zeile einen Zeitstempel voran, nötig zum Abgleich mit Host-Logs
--since 10m nur die letzten 10 Minuten; absolute Zeiten wie 2026-10-29T10:00:00 gehen auch
--until zusammen mit --since schneidet ein Fenster heraus
-i bei grep heißt Groß-/Kleinschreibung ignorieren, error / ERROR / Error werden alle erfasst
Logs liegen in /var/lib/docker/containers/<ID>/<ID>-json.log und verschwinden mit dem Container —
wichtige Dienste nach journald oder in einen Collector schicken, nicht nur in Docker lassen

docker logs -f --tail 200 -t myapp
docker logs --since 10m myapp | grep -i error

docker exec: eine Shell im Container

-it interaktives Terminal; ohne -t kein Prompt, ohne -i keine Eingabe — beide zusammen
-u root als root hinein, wenn der Standardbenutzer des Images die Konfiguration nicht lesen kann
-w /app setzt das Arbeitsverzeichnis, dann entfällt das cd nach dem Einstieg
-e KEY=val injiziert eine Variable temporär, um zu prüfen, ob eine nicht ankam
-- stoppt die Auswertung durch docker; nötig, wenn das Kommando selbst mit - beginnt
-c bei bash führt den Rest als Kommandostring aus, bash -c 'a && b' macht mehrere Schritte auf einmal
-l bei ls ist das Langformat mit Rechten und Eigentümer
Scheitert exec, zuerst prüfen, ob der Container läuft: steht er in docker ps -a auf Exited, scheitert exec immer,
das eigentliche Problem ist der bereits beendete Hauptprozess
Kein bash im Image? sh probieren — alpine-basierte Images haben meist nur sh
Distroless-Images haben gar keine Shell; dann docker cp für Dateien oder ein temporärer Debug-Container

docker exec -it -u root myapp sh
docker exec myapp env | grep -i database
docker exec -it myapp bash -c 'ls -l /app'

docker inspect: die wirksame Konfiguration

--format wertet ein Go-Template aus, -f ist die Kurzform
Nützliche Pfade: .State.Status Laufzustand, .State.ExitCode Exit-Code,
.NetworkSettings.Networks für die Container-IP, .Mounts für Mounts, .Config.Env für Variablen,
.HostConfig.RestartPolicy für die Neustartstrategie
{{json .}} gibt rohes JSON aus, besser für jq
--type image prüft ein Image statt eines Containers
Werte aus compose können per -e auf der Kommandozeile überschrieben sein; nur inspect zeigt, was wirklich gilt
.Config.Env enthält oft Passwörter und Token — vor dem Weitergeben filtern

docker inspect -f '{{.State.Status}} exit={{.State.ExitCode}}' myapp
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' myapp
docker inspect --format '{{json .Mounts}}' myapp | jq

docker stats: Ressourcen live

--no-stream nimmt einen Snapshot und beendet, gut für Skripte und periodische Aufzeichnung
--format eigene Spalten, leicht zu greppen oder in eine Datenbank zu laden
-a nimmt gestoppte Container mit auf (sie zeigen 0, stehen aber in der Liste)
MEM LIMIT ist die Obergrenze des Containers; MEM % nahe 100 heißt OOM steht bevor
CPU % ist auf die Kernzahl normiert, über 100% bei einem Container auf Mehrkern-Hosts ist normal
Bei OOMKilled zeigt stats nichts (der Prozess ist weg); dann ExitCode 137 via docker inspect prüfen
und die oom-kill-Zeilen in dmesg

docker stats --no-stream --format '{{.Name}} {{.CPUPerc}} {{.MemUsage}}'
docker stats --no-stream -a

docker build: Builds und Cache

-t taggt das Image; name:version statt nur latest
-f verweist auf ein Dockerfile außerhalb des Build-Verzeichnisses
--no-cache ignoriert den Cache komplett — erster Versuch bei „Code geändert, Image unverändert“
--build-arg übergibt ein Build-Argument; wirkt nur, wenn das Dockerfile es per ARG deklariert
--target stoppt an einer bestimmten Stage, gut zum Debuggen von Multi-Stage-Builds
--progress=plain druckt das volle Build-Log; der TTY-Standard faltet parallele Ausgaben in eine Zeile und verbirgt Fehler
Das abschließende . ist der Build-Kontext, nicht der Dockerfile-Pfad — ein großer Kontext bremst, also node_modules und .git per .dockerignore ausschließen

docker build -t myapp:1.2.3 .
docker build -f docker/Dockerfile.prod --build-arg NODE_ENV=prod --progress=plain -t myapp:prod .

docker compose: Stack starten und Logs lesen

-f wählt die compose-Datei; für mehrere Umgebungen zwei -f stapeln, das spätere gewinnt
-p setzt den Projektnamen — Standard ist der Verzeichnisname, mehrere Stacks auf einem Host brauchen ihn explizit, sonst kollidieren Container und Netze
-d läuft im Hintergrund; ohne -d hängt compose am Terminal und Ctrl+C stoppt den Stack
--build baut die Images vor dem Start neu
up erstellt und startet; down stoppt und entfernt Container und Netze
logs -f --tail 100 folgt den Logs; ein Dienstname am Ende beschränkt auf diesen
config prüft die Syntax und druckt die zusammengeführte Endkonfiguration — nach Änderungen erst das, sicherer als direkt up
compose v2 ist docker compose (Leerzeichen), v1 ist docker-compose (Bindestrich); die Optionen weichen leicht ab, im Skript prüfen

docker compose -f docker-compose.yml -f docker-compose.prod.yml -p shop up -d --build
docker compose -p shop logs -f --tail 100 api
docker compose -p shop config

Aufräumen: Images und Container-Logs füllen die Platte

system df fasst den Verbrauch nach Images / Containern / Volumes / Build-Cache zusammen, -v schlüsselt je Objekt auf
prune entfernt ungenutzte Objekte; standardmäßig nur Dangling Images (<none>), mit -a alle Images ohne Container-Referenz
--volumes räumt auch Volumes auf — dort kann eine Datenbank liegen, das ist nicht wiederherstellbar
--filter until=24h löscht nur Älteres als 24 Stunden, damit nichts Laufendes getroffen wird
Reihenfolge: erst system df -v, um den Verursacher zu sehen, dann gezielt prune; nicht mit system prune -a beginnen
Container-Logs füllen Platten ebenfalls: /var/lib/docker/containers/*/*-json.log
Die echte Lösung sind log-opts mit max-size und max-file in daemon.json

docker system df -v
docker image prune -a --filter until=24h
docker system prune --volumes

☸️ Kubernetes kompakt (8)

Die K8s-Reihenfolge ist fast fix: Status, Events, Logs, Shell. Jeder übersprungene Schritt ist Raten

kubectl get: was existiert, wo, in welchem Zustand

-A alle Namespaces — zuerst setzen, wenn eine Ressource „fehlt“; meist ist es der falsche ns
-n wählt den Namespace; ohne Angabe gilt der Standard des Contexts
-o wide ergänzt IP und Node als Spalten, unverzichtbar bei Netz- und Scheduling-Problemen
-o yaml druckt die vollständige Definition; vor Änderungen exportieren
--show-labels zeigt Labels — wenn ein Service seine Pods nicht findet, passt fast immer ein Label nicht
-l app=api filtert nach Label, verlässlicher als grep auf Namen
--sort-by=.status.startTime sortiert nach Startzeit, neueste zuletzt
Kurznamen existieren: pod/po, svc, deploy, rs, no, cm, secret

kubectl get pod -A -o wide
kubectl get pod -n shop -l app=api --show-labels
kubectl get pod -A --sort-by=.status.startTime

kubectl describe: das erste Kommando bei Problemen

Der Events-Block unten ist der informationsreichste Teil der K8s-Fehlersuche —
ImagePullBackOff, FailedScheduling, Unhealthy und OOMKilled stehen mit Grund dort
Ohne -n im falschen Namespace landet man bei einem nackten NotFound ohne Hinweis
Events werden standardmäßig etwa eine Stunde behalten, also früh mitschneiden statt später suchen
describe node endet mit Allocated resources und zeigt request / limit in Prozent,
bleibt ein Pod Pending, prüfen, ob Requests den Node belegen (nicht ob die CPU wirklich lastet)
describe zeigt nur Konfiguration und Events, nie Anwendungslogs — dafür ist kubectl logs da

kubectl describe pod -n shop api-7d9f-abc12
kubectl describe node k8s-node-2 | tail -30

kubectl logs: aktuelle Logs und der Absturz davor

-f folgt dem Stream
-n wählt den Namespace; ohne Angabe gilt der Standard des Contexts
--tail 200 die letzten 200 Zeilen; langlebige Pods fluten sonst das Terminal
-c wählt den Container — bei Sidecar im selben Pod Pflicht, sonst kommt ein ambiguous-Fehler mit Containerliste
--previous liest den vorherigen Container: CrashLoopBackOff startet den Container immer neu,
das aktuelle Log zeigt nur „kurz nach Start gestorben“, die echte Ursache steht im vorherigen
--since 10m nur die letzten 10 Minuten
--timestamps stellt jeder Zeile einen Zeitstempel voran
Ressourcen akzeptieren typ/name wie deploy/api, um die Pod-Logs eines Deployments direkt zu lesen

kubectl logs -f --tail 200 -n shop deploy/api
kubectl logs -n shop api-7d9f-abc12 --previous
kubectl logs -n shop api-7d9f-abc12 -c istio-proxy --since 10m

kubectl exec / cp: Shell im Container und Dateitransfer

-it interaktives Terminal, beide Optionen zusammen
-n wählt den Namespace
-c wählt den Container, bei Multi-Container-Pods Pflicht
-- stoppt die Auswertung durch kubectl; nötig, wenn das Kommando mit - beginnt
cp nimmt Quelle und Ziel, die Pod-Seite als pod:pfad
cp braucht tar im Container, Distroless-Images scheitern direkt
kubectl debug injiziert einen temporären Debug-Container im selben Netzwerk-Namespace,
--image wählt das Debug-Image (busybox genügt), --target den zu debuggenden Container —
ohne Shell im Produktions-Image ist das der richtige Weg, viel schneller als ein neues Image

kubectl exec -it -n shop api-7d9f-abc12 -- sh
kubectl cp -n shop api-7d9f-abc12:/app/logs/app.log ./app.log
kubectl debug -it -n shop api-7d9f-abc12 --image=busybox --target=api

kubectl get events: Ereignisse auf Clusterebene

Gleiche Quelle wie der Events-Block in describe, aber über Ressourcen hinweg lesbar — gut, wenn das kaputte Objekt noch unbekannt ist
-n wählt den Namespace
--sort-by=.lastTimestamp sortiert nach Zeit, neueste zuletzt
--field-selector type=Warning zeigt nur Warnungen, eine Größenordnung weniger Rauschen
--field-selector involvedObject.name=api-7d9f-abc12 grenzt auf ein Objekt ein
-A alle Namespaces; Probleme auf Clusterebene (Node NotReady, Evictions) erscheinen nur damit
FailedScheduling trägt die Worte des Schedulers, etwa
insufficient cpu oder node(s) had taint — es sagt direkt, warum der Pod nicht platziert wird,
statt zwischen Ressourcen- und Taint-Problem zu raten
Events haben eine TTL von etwa einer Stunde, im Restart-Loop also zügig mitschneiden

kubectl get events -A --sort-by=.lastTimestamp | tail -20
kubectl get events -n shop --field-selector type=Warning
kubectl get events -n shop --field-selector involvedObject.name=api-7d9f-abc12

kubectl apply: deployen, zurückrollen, neu starten

-f zeigt auf eine Manifest-Datei oder ein Verzeichnis; -f - liest von stdin
--dry-run=client sendet nicht, druckt nur das endgültige YAML — zum Erzeugen von Vorlagen statt Handarbeit
--dry-run=server prüft auf dem Server (inklusive Admission-Webhooks), deutlich strenger als client
-o yaml macht die Dry-Run-Ausgabe erst nützlich
rollout status wartet auf den Abschluss und nennt genau die Stelle, an der es hängt
rollout undo rollt auf die vorherige Revision zurück; --to-revision=3 wählt eine bestimmte, Nummern mit history finden
rollout restart startet Pods neu, ohne Konfiguration zu ändern — der übliche Schritt nach ConfigMap oder Secret
-n wählt den Namespace
apply ist deklarativ und idempotent; create ist imperativ und scheitert beim zweiten Mal mit AlreadyExists

kubectl apply -f deploy/api.yaml --dry-run=client -o yaml > api.gen.yaml
kubectl rollout status deploy/api -n shop
kubectl rollout undo deploy/api -n shop --to-revision=3
kubectl rollout restart deploy/api -n shop

kubectl port-forward: Cluster-Dienst lokal prüfen

Format ist ressourcentyp/name lokalport:remoteport; NodePort oder Ingress sind nicht nötig
-n wählt den Namespace
--address 0.0.0.0 erlaubt auch anderen Maschinen im Subnetz den Zugriff; standardmäßig nur 127.0.0.1
Es tunnelt über kube-apiserver und umgeht den Service —
„läuft die Anwendung“ und „blockiert eine NetworkPolicy“ sind also zwei Fragen, diese klärt die erste
Terminal schließen beendet es; für lange Sitzungen in tmux oder screen legen
Reihenfolge: port-forward klappt heißt Anwendung in Ordnung und das Problem liegt bei Ingress / Service / NetworkPolicy;
port-forward scheitert heißt Pod oder Port selbst ist defekt

kubectl port-forward -n shop svc/api 8080:80
kubectl port-forward -n shop --address 0.0.0.0 svc/api 8080:80
kubectl port-forward -n shop pod/api-7d9f-abc12 5432:5432

kubectl top / drain: Node-Ressourcen und Wartung

top node und top pod brauchen metrics-server; ohne ihn kommt Metrics API not available
--sort-by=cpu oder --sort-by=memory sortiert die Liste zu den großen Verbrauchern
--containers bricht einen Pod auf Container herunter und zeigt, welcher Sidecar den Speicher frisst
-A alle Namespaces
Allocated resources in describe node ist das request / limit-Verhältnis —
bei Scheduling-Fehlern zuerst hier schauen: zu große Requests buchen den Node aus, während die CPU idle ist
cordon markiert einen Node als nicht schedulbar, ohne laufende Pods zu berühren
drain evakuiert Pods für Wartung; --ignore-daemonsets überspringt DaemonSets
(die nie evakuiert werden sollten), --delete-emptydir-data erlaubt das Entfernen von Pods mit emptyDir (deren Scratch-Daten gehen verloren)
uncordon stellt das Scheduling danach wieder her

kubectl top node
kubectl top pod -A --sort-by=memory --containers
kubectl drain k8s-node-2 --ignore-daemonsets --delete-emptydir-data

🔬 Tracing auf Prozessebene (strace / perf) (7)

top sagt, welcher Prozess CPU verbraucht, nicht worauf er wartet. Diese Werkzeuge beantworten die zweite Frage

strace -p: in welchem Systemaufruf hängt der Prozess

-p hängt sich an einen laufenden Prozess; ein vorangestelltes Kommando wird ab dem Start verfolgt
-f folgt geforkten Kindern — ohne es sieht man nur den Vater warten, während die Arbeit woanders passiert
-T zeigt die Dauer jedes Aufrufs (<0.000012>) — so findet man den langsamen Schritt
-tt Zeitstempel in Mikrosekunden
-s 200 zeigt bis 200 Zeichen pro String; die Standard-32 kürzt wichtige Pfade zu ...
-o out.txt schreibt in eine Datei; Zehntausende Zeilen sind im Terminal unlesbar
-c fasst beim Beenden Anzahl und Gesamtzeit je Systemaufruf zusammen — erst damit die teure Klasse finden,
dann mit -T den einzelnen langsamen Aufruf eingrenzen
Hängt -p und es kommt gar keine Ausgabe, steckt der Prozess in uninterruptible IO oder einem Kernel-Lock;
dann /proc/PID/stack und /proc/PID/wchan ansehen
In Produktion vorsichtig: -f auf allen Aufrufen kann einen Prozess um ein Vielfaches bremsen, also -c oder Filter nutzen

strace -p 1234 -T -tt -s 200
strace -f -p 1234 -o /tmp/st.txt
strace -c -p 1234

strace -e trace: nur die relevanten Aufrufe

-e trace= ist die nützlichste strace-Option überhaupt
-e trace=file nur dateibezogene Aufrufe (openat, stat, unlink) — für „Datei nicht gefunden“ oder Rechte
-e trace=network nur Netzwerkaufrufe (connect, sendto, recvfrom) — für „keine Verbindung“ oder Timeouts
-e trace=openat,connect kommagetrennt für wenige bestimmte Aufrufe
-e trace=desc nur Dateideskriptor-Operationen, um zu sehen, welcher fd blockiert
-e signal=SIGSEGV nur einem Signal folgen
Filtern drückt Zehntausende Zeilen auf Dutzende — richtige Reihenfolge: erst -c für die teure Klasse,
dann -e trace= nur für diese Klasse
openat mit ENOENT heißt Pfad fehlt, EACCES heißt Rechte fehlen, EAGAIN heißt nicht-blockierend ohne Daten;
die drei brauchen völlig verschiedene Lösungen, also nicht jeden Fehler als Rechteproblem behandeln
Alles andere wie im strace -p-Eintrag: -f folgt Kindern, -p zielt auf einen Prozess, -T zeigt Zeit pro Aufruf, -tt gibt Mikrosekunden, -o schreibt in eine Datei

strace -f -e trace=file -p 1234 -T
strace -f -e trace=network -p 1234 -tt
strace -o net.txt -e trace=openat,connect -f ./app

perf top: welche Funktion verbrennt die CPU

perf top ist ein Live-Profil der heißesten Funktionen; keine PID nötig, es deckt die ganze Maschine ab
-p beschränkt auf einen Prozess
-F 99 sampelt mit 99Hz — runde 100Hz meiden, sonst entstehen durch Timer-Aliasing falsche Hotspots
-g zeigt einen Aufrufgraphen; die Ansicht ist hier begrenzt, echte Ketten brauchen record
unknown als Funktionsname heißt fehlendes debuginfo oder gestrippte Symbole —
perf ist nicht defekt, das Binary kam ohne Symboltabelle
Braucht root oder ein gelockertes kernel.perf_event_paranoid; in Containern meist --privileged oder SYS_ADMIN
Unterschied zu top: top beantwortet „welcher Prozess“, perf top „welche Funktion“ — Letzteres ist der Start der Optimierung

perf top
perf top -p 1234 -F 99
perf top -g

perf record: Daten für Flame Graphs

record nimmt Samples auf, report sieht sie interaktiv an, script gibt Text aus — das Trio
-F 99 sampelt 99-mal pro Sekunde
-g zeichnet Aufrufketten auf; für Flame Graphs Pflicht, sonst nur isolierte Funktionsnamen
-p zielt auf einen Prozess; -a sampelt jede CPU systemweit
--call-graph dwarf entrollt mit DWARF-Infos, genauer als das Standard-fp,
kostet aber eine Größenordnung mehr Daten; mit -fomit-frame-pointer gebaute Binaries brauchen es zwingend
-o Ausgabedatei, Standard ist perf.data im aktuellen Verzeichnis
-i sagt report oder script, welche Datei zu lesen ist
-- davor steht ein Kommando, damit perf es startet und die Startphase sampelt
Flame Graphs brauchen die gefaltete Ausgabe von script, weiterverarbeitet mit stackcollapse aus FlameGraph
In Containern ist perf_event_paranoid meist 2, das verbirgt Kernelsymbole;
sysctl kernel.perf_event_paranoid=-1 öffnet es

perf record -F 99 -g -p 1234 -o /tmp/perf.data
perf report -i /tmp/perf.data
perf script -i /tmp/perf.data > /tmp/perf.txt

pidstat: CPU, IO und Speicher je Prozess

pidstat 1 gibt jede Sekunde aus und aktualisiert laufend; eine zweite Zahl begrenzt die Durchläufe
-p ALL zeigt alle Prozesse; -p 1234 einen bestimmten
-d Platten-IO (kB_rd/s lesen, kB_wr/s schreiben, iodelay in Millisekunden)
-r Speicher (Page Faults, RSS)
-u CPU-Auslastung (der Standardmodus)
-t Aufteilung je Thread, um den Thread mit der CPU-Last zu finden — für Multithread-Programme Pflicht
-w Context Switches: cswch/s freiwillig (warten auf Ressource), nvcswch/s unfreiwillig (verdrängt)
Teil des Pakets sysstat, minimale Installationen brauchen es zuerst
Besser als top für lange Abtastung in festem Intervall in eine Datei, um danach Mittelwerte zu rechnen

pidstat -p 1234 1 10
pidstat -d -p ALL 1 5
pidstat -t -p 1234 1

iostat / vmstat: Platten- und Speicherengpässe

iostat -x 1 erweiterte Statistik, jede Sekunde aktualisiert
-x druckt die erweiterten Kennzahlen; ohne es kommt eine andere, viel dünnere Tabelle
%util nahe 100% heißt Platte ausgelastet; await ist die mittlere Wartezeit pro IO in Millisekunden
rkB/s und wkB/s sind der Durchsatz — hohes await mit niedrigem Durchsatz heißt langsame Platte,
hohes await mit hohem Durchsatz heißt echte Last; beide brauchen andere Kapazitätsmaßnahmen
vmstat 1 eine Zeile pro Sekunde: r ist die Run-Queue (dauerhaft über der Kernzahl heißt CPU-Mangel),
b blockierte Prozesse, si / so Swap rein und raus (ungleich 0 heißt Speicherdruck)
wa ist der Anteil CPU-Zeit im IO-Warten; hohes wa zeigt auf die Platte, nicht die CPU
-d heißt hier Plattenstatistik und zeigt nur die Platten-Spalten
Die erste Zeile ist der Durchschnitt seit dem Boot; Echtzeitwerte beginnen in Zeile zwei — nie aus Zeile eins schließen

iostat -x 1 5
vmstat 1 5
vmstat -d 1

gdb / gcore: Stacktrace bei hängendem Prozess

Hängt ein Prozess und strace gibt nichts aus (uninterruptibler Zustand), braucht man den Userspace-Stack
gdb -p pausiert den Prozess während des Anhängens — vor dem Beenden detachen, sonst bleibt er stehen, was einem Ausfall gleichkommt
-ex führt ein gdb-Kommando aus und macht weiter; mehrere aneinandergehängt ergeben Anhängen, Stack dumpen, Detachen in einem
thread apply all bt dumpt die vollen Stacks aller Threads; bei Single-Thread-Programmen genügt bt
gcore schreibt eine Core-Datei für die Analyse danach, sicherer als Live-Debugging
-o setzt den Ausgabepfad der Core-Datei (ohne .PID-Suffix)
Bei einer JVM kein gdb — es kann Java-Stacks nicht lesen, dafür jstack
perf --call-graph sampelt Punkte über die Zeit; ein Hänger braucht den vollen Stack, keines ersetzt das andere

gdb -p 1234 -ex 'thread apply all bt' -ex detach
gcore -o /tmp/core.1234 1234

⌨️ Vim und Terminal-Effizienz (6)

Auf Servern gibt es oft nur vi, an der Konfiguration führt kein Weg vorbei. Zwanzig Tasten genügen, man muss nicht alles lernen

Öffnen, speichern, beenden

vim +120 DATEI springt direkt zu Zeile 120 — der schnellste Weg, einer Zeilennummer aus dem Log zu folgen
-u NONE lädt keine Konfiguration und keine Plugins, um ein kaputtes vimrc auszuschließen
-w /tmp/keys.log zeichnet jeden Tastendruck auf, um zu sehen, was man wirklich gedrückt hat
:w speichern; :w! erzwungen speichern (read-only oder fehlende Rechte)
:q beenden, bei ungespeicherten Änderungen verweigert; :q! verwirft und beendet
:wq oder :x speichern und beenden; :x schreibt nicht, wenn nichts geändert wurde
:e! verwirft Änderungen und lädt neu — wenn die Datei zerschossen ist und man neu anfangen will
:saveas /tmp/a.conf speichern unter, der Standardschritt vor Konfigurationsänderungen
Versehentlich eine read-only Datei bearbeitet? :q! ist die schnellste Rettung — nicht nach Speichern suchen

vim +120 /etc/nginx/nginx.conf
vim -u NONE /etc/nginx/nginx.conf

:w      保存        :q!     ohne Speichern verlassen
:wq     保存退出    :x      nur bei Änderungen speichern
:e!     重新加载    :saveas /tmp/a.conf  speichern unter

Bewegen: lesen ohne ändern

gg erste Zeile; G letzte Zeile; 42G springt zu Zeile 42
0 Zeilenanfang; ^ erstes Nicht-Leerzeichen (für Einrückung); $ Zeilenende
w nächstes Wort; b vorheriges Wort; e Ende des aktuellen Wortes
Ctrl+f eine Seite vor; Ctrl+b zurück; Ctrl+d halbe Seite vor; Ctrl+u halbe zurück
% springt zur passenden Klammer — schnell das Ende eines Config-Blocks finden
'' zurück zur letzten Position (zwei Apostrophe); Enter geht auch
In großen Dateien keine Pfeiltasten; hjkl mit den Bewegungen oben ist viel schneller
Vor dem Bearbeiten :set nu, damit Zeilennummern aus Fehlermeldungen etwas bedeuten

gg        文件首行            G        letzte Zeile der Datei
42G       第 42 行            0        Zeilenanfang
^         行首首个非空白      $        Zeilenende
w / b / e 下词首 / 上词首 / 词尾
Ctrl+f    下一页              Ctrl+b   eine Seite zurück
%         跳到匹配的括号      ''       zur letzten Position zurückspringen

Bearbeiten: löschen, kopieren, einfügen, rückgängig

dd löscht eine Zeile — eigentlich ein Ausschneiden, p fügt sie wieder ein
yy kopiert eine Zeile; 5yy kopiert fünf Zeilen
p fügt unter der aktuellen Zeile ein; P darüber
u rückgängig; Ctrl+r wiederholen (wenn zu weit zurückgenommen)
x löscht ein Zeichen; r ersetzt das Zeichen unter dem Cursor ohne Insert-Modus
cc schreibt eine ganze Zeile neu, Inhalt weg, Einrückung bleibt
d$ löscht bis Zeilenende; d0 bis Zeilenanfang; dw ein Wort
. wiederholt die letzte Änderung — die wertvollste Taste in Vim; 20 Zeilen löschen ist dd plus 19-mal .
i fügt vor dem Cursor ein; a danach; o neue Zeile darunter; O darüber
Esc zurück in den Normalmodus — vergessen erzeugt eine Zeile Zeichensalat

dd / yy / p    Zeile löschen / Zeile kopieren / einfügen
5yy            5 Zeilen kopieren
u / Ctrl+r     rückgängig / wiederholen
x / r          Zeichen löschen / ein Zeichen ersetzen
cc             ganze Zeile überschreiben (Einrückung bleibt)
.              letzte Änderung wiederholen (die wertvollste Taste)
i / a / o / O  davor einfügen / danach einfügen / neue Zeile darunter / neue Zeile darüber

Blockauswahl und Massenbearbeitung

Ctrl+v startet die Blockauswahl; Cursor bewegen, um eine Spalte zu markieren — für Massenkommentare und Ausrichtung
Mit Block und I wird am Zeilenanfang aller Zeilen gleichzeitig eingefügt, wirksam erst mit Esc (ohne Esc sieht es nach nichts aus)
A hängt am Ende jeder markierten Zeile an
c ersetzt den ganzen Block, d löscht ihn
:g/pattern/d löscht alle passenden Zeilen
:g/pattern/s/alt/neu/g ersetzt nur in passenden Zeilen, sicherer als global
:sort sortiert die markierten Zeilen; :sort u sortiert und entfernt Duplikate
:normal @a führt das Makro aus Register a einmal je markierter Zeile aus, für Wiederholarbeit
:set nu zuerst, damit man den Bereich bei Blockoperationen nicht falsch schätzt

Ctrl+v  I  Esc      nach Blockauswahl an jeden Zeilenanfang einfügen (Sammelkommentar)
Ctrl+v  A  Esc      nach Blockauswahl an jedes Zeilenende anhängen
:g/^#/d             alle Zeilen löschen, die mit # beginnen
:g/ERROR/s/old/new/g  nur in Zeilen mit ERROR ersetzen
:sort u             markierte Zeilen sortieren und deduplizieren

Kaputtes Einfügen, Zeilenenden, Kodierung

Code aus Browser oder Windows eingefügt und die Einrückung wandert nach rechts: erst :set paste, danach :set nopaste
(auch :set pastetoggle=<F2> für Umschalten mit F2)
Auf Windows bearbeitete Datei zeigt ^M am Zeilenende: :set ff=unix und dann :w
:set ff allein zeigt, ob aktuell dos oder unix gilt
Mojibake bei chinesischem Text: :set fileencoding=utf-8 und speichern; wenn nur die Anzeige stimmen soll,
hilft :e ++enc=gbk zum erneuten Lesen als GBK
:w !sudo tee % löst „sudo vergessen, jetzt nicht speicherbar“ —
es schreibt den Puffer per sudo unter dem aktuellen Dateinamen zurück, schneller als neu öffnen
Alles davon gilt pro Sitzung; für dauerhaft in ~/.vimrc eintragen

:set paste           Auto-Einrückung vor dem Einfügen abschalten
:set nopaste         nach dem Einfügen wieder einschalten
:set ff=unix         Windows-^M entfernen
:set fileencoding=utf-8   Kodierung korrigieren und speichern
:w !sudo tee %      ohne sudo zwangsweise zurückschreiben

🔥 Playbooks zur Fehlersuche (6)

„Der Server ist langsam“ sind vier Probleme: CPU, Speicher, IO, Netzwerk. Ein Kommando trennt sie

Erster Blick: welche Ebene bremst

uptime zeigt Load average für 1/5/15 Minuten — über der Kernzahl heißt Warteschlange
Hohe Last bei leerer CPU (hohes %id in top) deutet auf IO-Warten — die wa-Spalte ansehen
In free -h die Spalte available lesen, nicht free (-h human readable)
df -h (Kapazität) und df -i (Inodes) zusammen prüfen; eine volle Platte erzeugt die seltsamsten Fehler
vmstat 1 5 sampelt einmal pro Sekunde, fünfmal — r (Run-Queue), b (blockiert), wa (IO-Warten), si/so (Swapping)
Erst die Ebene bestimmen, dann handeln — deutlich wirksamer als Dienste neu zu starten

uptime
free -h
df -h; df -i
vmstat 1 5

Hohe Last, wenig CPU

Die wa-Spalte in vmstat ansehen: dauerhaft über 10 heißt IO bremst
Dann Spalte b (blockierte Prozesse): über der Kernzahl hängen viele
Den Verursacher mit pidstat -d 1 5 finden (-d meldet nur IO-Statistik, jede Sekunde, fünfmal)
In ps -eo wählt -e alle Prozesse und -o die Spalten; die state-Spalte zeigt D (uninterruptible)
awk '$1=="D"' filtert Prozesse im D-Zustand — die, die auf IO warten
Typische Ursachen: Full Table Scans, synchrones Log-Schreiben, laufendes Backup, wackelige NFS- oder Cloud-Platten
Prozesse im D-Zustand lassen sich nicht killen — auf IO warten oder rebooten

vmstat 1 5
pidstat -d 1 5
ps -eo state,pid,cmd | awk '$1=="D"'

Reicht der Speicher wirklich

In free -h ist available das, was neue Prozesse noch bekommen; ein kleiner free-Wert ist normal, Linux nutzt ihn als Cache
Hohes buff/cache ist kein Problem, es wird bei Bedarf zurückgeholt
Viel Swap heißt echter Druck; dauerhaft si/so ungleich 0 in vmstat ist Thrashing
Prozesse sortieren: ps -eo pid,rss,comm --sort=-rss (-e alle, -o Spalten, Minus vor dem Schlüssel heißt absteigend)
RSS ist der echte Verbrauch, VSZ der Adressraum und weitgehend ohne Aussage
In Containern zählt das cgroup-Limit (/sys/fs/cgroup/memory.max), nicht das Host-Total
Bei Verdacht auf ein Leck zwei RSS-Snapshots zehn Minuten auseinander vergleichen und sehen, was nur wächst

free -h
vmstat 1 5    # si / so ansehen
ps -eo pid,rss,comm --sort=-rss | head

Langsamer Endpunkt: jeden Hop messen

Ebene 1: curl -w zerlegt DNS, Verbindung, Time-to-first-byte und Gesamtzeit
curl -o /dev/null verwirft den Body, -s stillt den Fortschritt, -w druckt ein eigenes Format — zusammen nur Zahlen
Ebene 2: curl 127.0.0.1 auf dem Server selbst, um Netz und Gateway auszuschließen
Ebene 3: App-Log-Zeiten mit nginx $request_time und $upstream_response_time vergleichen
Eine große Lücke heißt Verzögerung in einem Zwischen-Proxy oder Connection-Pool
Das Slow-Query-Log der Datenbank nicht vergessen — viele „langsame Endpunkte“ sind langsame Queries
Hohe Gesamtzeit bei niedriger Upstream-Zeit heißt meist Drosselung oder Warteschlange

curl -o /dev/null -s -w 'dns:%{time_namelookup} conn:%{time_connect} ttfb:%{time_starttransfer} total:%{time_total}\n' https://example.com/api
# in nginx log_format ergänzen:
# $request_time $upstream_response_time

Paketverlust und Jitter

ping -c 50 sendet 50 Pakete und meldet loss% plus mdev (Jitter); ein einzelner Ping beweist nichts
ping -M do -s 1472 prüft die Pfad-MTU: -M do verbietet Fragmentierung, -s setzt die Nutzlast
mtr -r -c 50: -r druckt einen Bericht statt eines Live-Bildes, -c setzt die Paketzahl, und es zeigt Verlust pro Hop
ICMP-Rate-Limiting kann Verlust vortäuschen; TCP-Sonden (nc -z, curl) sind verlässlicher
ethtool -S NIC zeigt rx/tx-Fehler- und Drop-Zähler (-S ist statistics)
grep -iE heißt Groß-/Kleinschreibung ignorieren plus erweiterter Regex
Verlust im selben Rack deutet auf NIC oder Kabel, über Regionen auf die Leitung des Carriers

ping -c 50 10.0.0.5
mtr -r -c 50 10.0.0.5
ethtool -S eth0 | grep -iE 'err|drop'

Health-Check in einem Rutsch

Auf einer fremden Maschine zuerst das laufen lassen, um das Gesamtbild zu bekommen, dann entscheiden
Jede Zeile ist beschriftet mit der Frage, die sie beantwortet — nicht als Blackbox kopieren
Options-Spickzettel (alles oben erklärt): --sort=-%cpu absteigend nach CPU, -h human readable, -i Inodes
grep -vE heißt invertieren plus erweiterter Regex, hier um virtuelle Dateisysteme wie tmpfs auszublenden
ss -tulnp zerlegt sich als t/TCP, u/UDP, l/listening, n/numerisch, p/Prozess
Bei systemctl zeigt --failed nur defekte Units und --no-pager vermeidet Paging
Bei dmesg liefert -T lesbare Zeit und -l filtert nach Stufe
Alles hier ist read-only, auf Produktion unbedenklich und direkt ins Ticket kopierbar

echo "== 负载 =="; uptime
echo "== CPU 前 5 =="; ps aux --sort=-%cpu | head -6
echo "== 内存前 5 =="; ps aux --sort=-%mem | head -6
echo "== 内存 =="; free -h
echo "== 磁盘 =="; df -h | grep -vE 'tmpfs|overlay'
echo "== inode =="; df -i | grep -vE 'tmpfs|overlay'
echo "== 监听端口 =="; ss -tulnp
echo "== 失败服务 =="; systemctl list-units --failed --no-pager
echo "== 内核错误 =="; dmesg -T -l err | tail -10