104 Befehle, die man wirklich tippt — jede Option erklärt, nach Vorhaben gruppiert
Suchen, stapelweise bearbeiten, packen — ein falscher Parameter kostet meist eine gelöschte Datei oder übersehene versteckte Verzeichnisse
-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
-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
-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
-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
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)
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 -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 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
Konfiguration ändern, Spalten auswerten, Kommandos bauen — alles, was 200 Zeilen berührt, gehört zu sed/awk
-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
-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 -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 | headcut -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 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 echotee 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 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.jsondiff -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
Login, Schlüssel, Übertragung, Fortsetzen — der meiste Zeitaufwand liegt meist darin, die Datei überhaupt dorthin zu bekommen
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 -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
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-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 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 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
-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 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
Numerische Bits, Spezialbits, sudo — falsche Rechte zeigen sich als „Passwort stimmt, Zugriff verweigert“ oder „Dienst startet nicht“
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 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
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 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 -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 -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
Wer verbraucht CPU, wer hält die Datei, wie beendet man sauber — der typische Fehler hier ist ein zu frühes kill -9
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
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 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
& 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 -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 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 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 -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
Wenn ein Dienst nicht antwortet: erst trennen zwischen „lauscht nicht“, „nicht freigegeben“ und „nicht erreichbar“ — falsche Reihenfolge kostet viel Zeit
-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
-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/apinc -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 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
+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
-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'
-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-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
„Platte voll“ hat vier verschiedene Ursachen: viele Dateien, eine große Datei, erschöpfte Inodes, gelöschte aber offene Dateien
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"}'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
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 /
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
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 scheitern meist an PATH und Output-Umleitung; eine falsche Uhrzeit zieht Zertifikate, JWTs und Logs mit
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
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 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 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)
Die Reihenfolge zählt mehr als das Kommando: erst das Zeitfenster eingrenzen, dann per Stichwort verengen, dann den Kontext lesen
-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
-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
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'
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
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/appFast jeder Startfehler ist einer von drei: falscher Pfad, falscher Benutzer, fehlende Abhängigkeit — status sagt welcher
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
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
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
.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
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
Vor dem Installieren die Quelle prüfen, vor dem Entfernen sehen, was mitgeht — besonders auf Produktionsmaschinen
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 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
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
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
Neun von zehn Container-Problemen sind ein Unterschied zwischen vermuteter und tatsächlicher Konfiguration — erst mit inspect die Fakten abgleichen, dann Logs
-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}}'-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
-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'
--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--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-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 .
-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
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
Die K8s-Reihenfolge ist fast fix: Status, Events, Logs, Shell. Jeder übersprungene Schritt ist Raten
-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
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
-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
-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
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
-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
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
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
top sagt, welcher Prozess CPU verbraucht, nicht worauf er wartet. Diese Werkzeuge beantworten die zweite Frage
-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
-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 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
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 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 -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
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
Auf Servern gibt es oft nur vi, an der Konfiguration führt kein Weg vorbei. Zwanzig Tasten genügen, man muss nicht alles lernen
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
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
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
/pattern sucht vorwärts; ?pattern rückwärts
n nächster Treffer; N vorheriger
* sucht das Wort unter dem Cursor ohne Eingabe; # rückwärts
:set hlsearch hebt alle Treffer hervor; :noh löscht die Hervorhebung (bei gelbem Bildschirm)
:%s/alt/neu/g ersetzt überall; g heißt jeder Treffer einer Zeile, ohne g nur der erste
:%s/alt/neu/gc fragt vor jedem Ersetzen — bei Konfigurationsdateien Pflicht, ein falsches Ersetzen legt den Dienst lahm
:10,20s/alt/neu/g beschränkt auf Zeile 10 bis 20
:.,$s/alt/neu/g von der aktuellen Zeile bis Dateiende
Das Trennzeichen ist austauschbar: :s#/usr/local/alt#/usr/local/neu#g spart Escaping der Slashes im Pfad
Vor dem Ersetzen mit /pattern die Trefferzahl prüfen, dann weiß man, was kommt
:%s/old/new/g global ersetzen (alle Treffer je Zeile) :%s/old/new/gc vor jedem Ersetzen bestätigen (bei Konfigurationen immer c) :10,20s/old/new/g nur die Zeilen 10-20 ersetzen :.,$s/old/new/g von der aktuellen Zeile bis zum Dateiende :s#/usr/local/old#/usr/local/new#g anderes Trennzeichen, Pfade ohne Escaping :noh Suchhervorhebung aufheben
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
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
„Der Server ist langsam“ sind vier Probleme: CPU, Speicher, IO, Netzwerk. Ein Kommando trennt sie
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
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"'
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
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_timeping -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'
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