linux.8x.wtf

Lerne Linux da,
wo Linux wohnt.

Die wichtigsten Befehle, erklärt an echten Situationen aus dem Alltag von Sysadmins, DevOps-Engineers und Entwicklern. Mit eingebautem Übungs-Terminal, in dem du alles sofort selbst tippst. Kostenlos, ohne Anmeldung, eine einzige Seite.

Kein Linux installiert? Egal. Das Terminal hier unten ist echt genug zum Lernen.

tty1 — linux.8x.wtf80×24
AMBER-PHOSPHOR · P3
man linux8xSeite 1
NAME linux8x - Linux-Befehle lernen, ohne einzuschlafen SYNOPSIS linux8x [--lernpfad] [--terminal] [--szenarien] DESCRIPTION Kuratierte Befehle statt A-bis-Z-Referenz. Jeder Befehl kommt mit Syntax, einem realistischen Beispiel samt echter Ausgabe und einer Situation, in der er dir den Tag rettet. Danach: Wissens-Check und Missionen im eingebauten Terminal. EXAMPLES Das Deploy-Log läuft voll? tail -f app.log Platte voll, aber wovon? du -sh * | sort -rh Wer hämmert auf deine API? awk '{print $1}' access.log | sort | uniq -c SEE ALSO python(8x), terminal(101)

Lernpfad

Zehn Module, aufgebaut wie eine Boot-Sequenz: Du arbeitest dich vom ersten pwd bis zur Produktions-Forensik vor. Abgeschlossene Module melden sich mit [ OK ].

01 · Survival-Basics

Du landest per SSH auf einem fremden Server. Keine Oberfläche, nur ein blinkender Cursor. Die ersten drei Fragen sind immer gleich: Wo bin ich? Was liegt hier? Wie komme ich woanders hin? Diese acht Befehle beantworten sie, und sie sind 80 Prozent deines Terminal-Alltags.

pwd

Wo bin ich gerade?
SYNTAX
pwd

Kurz für print working directory. Keine Optionen nötig, einfach tippen.

MERKE: Der Prompt zeigt oft nur ~. Das ist die Abkürzung für dein Home-Verzeichnis, pwd zeigt dir den vollen Pfad dahinter.
IM EINSATZ
ich@web-01:~bash
$ pwd /home/ich $ cd /var/log && pwd /var/log

ls

Was liegt hier?
SYNTAX
ls [-lah] [pfad]

-l zeigt Details (Rechte, Besitzer, Größe, Datum), -a auch versteckte Dateien (alles mit Punkt am Anfang), -h macht Größen lesbar (4,0K statt 4096). -t sortiert nach Änderungszeit.

MERKE: ls -lah als Muskelgedächtnis. Auf vielen Servern gibt es dafür schon den Alias ll.
IM EINSATZ
ich@web-01:~bash
$ ls -lah insgesamt 28K drwxr-xr-x 4 ich ich 4,0K 22. Aug 09:14 . -rw------- 1 ich ich 2,1K 22. Aug 09:14 .bash_history drwxr-xr-x 2 ich ich 4,0K 21. Aug 18:02 logs drwxr-xr-x 3 ich ich 4,0K 20. Aug 11:47 projekte -rw-r--r-- 1 ich ich 312 22. Aug 08:55 notizen.txt

cd

Woanders hin.
SYNTAX
cd [ziel]

cd .. geht eine Ebene hoch, cd ohne Ziel bringt dich heim, cd - springt ins vorherige Verzeichnis zurück. Der Unterstrich-Klassiker beim Pendeln zwischen zwei Pfaden.

IM EINSATZ
ich@web-01:~bash
$ cd /var/log/nginx $ cd - /home/ich $ cd - /var/log/nginx

cat

Datei komplett anzeigen.
SYNTAX
cat datei [datei2 …]

Perfekt für kurze Dateien: Configs, Hostnamen, Umgebungsdateien. Bei langen Dateien nimmst du less, sonst rauscht alles durch.

IM EINSATZ
ich@web-01:~bash
$ cat /etc/hostname web-01 $ cat /etc/os-release PRETTY_NAME="Debian GNU/Linux 13 (trixie)" ID=debian

less

Durch lange Dateien blättern.
SYNTAX
less datei

Drinnen navigierst du mit den Pfeiltasten. /wort sucht vorwärts, n springt zum nächsten Treffer, G ans Ende, q beendet. less +F folgt live wie tail -f, mit Ctrl-C kannst du zurückscrollen.

MERKE: Wenn du in einem Vollbild-Ding festhängst und nichts mehr geht: q ist fast immer der Ausgang.
IM EINSATZ
ich@web-01:/var/logless
Aug 22 09:14:02 web-01 systemd[1]: Started nginx.service Aug 22 09:14:07 web-01 kernel: [ OK ] eth0: link up Aug 22 09:15:31 web-01 sshd[1042]: Accepted publickey for ich /var/log/syslog Zeile 1-3/8421 /error ← so suchst du

tail & head

Ende oder Anfang einer Datei.
SYNTAX
tail [-n zahl] [-f] datei

tail zeigt die letzten 10 Zeilen, head die ersten. -n 50 ändert die Anzahl. Der Star ist tail -f: Er bleibt dran und zeigt jede neue Zeile sofort. So schaust du einem Log live zu.

MERKE: tail -f beendest du mit Ctrl-C. Das bricht nur das Zuschauen ab, dem Log passiert nichts.
IM EINSATZ
ich@web-01:~/logsbash
$ tail -n 3 app.log 2026-08-22 09:41:02 INFO request served in 84ms 2026-08-22 09:41:09 ERROR db timeout after 5000ms 2026-08-22 09:41:10 INFO retry ok $ tail -f app.log … wartet auf neue Zeilen (Ctrl-C zum Beenden)

man

Das eingebaute Handbuch.
SYNTAX
man befehl · man -k stichwort

man ls öffnet die Handbuchseite (Navigation wie in less). Kennst du den Befehlsnamen nicht, sucht man -k kopieren (alias apropos) in allen Kurzbeschreibungen. Für die Schnellversion: befehl --help.

IM EINSATZ
ich@web-01:~bash
$ man -k rename mv (1) - Dateien verschieben oder umbenennen rename (1) - mehrere Dateien umbenennen $ df --help | head -n 2 Aufruf: df [OPTION]… [DATEI]… Zeigt die Belegung der Dateisysteme an.

history

Was habe ich vorhin getippt?
SYNTAX
history · !! · Ctrl-R

history listet deine letzten Befehle nummeriert. !42 führt Nummer 42 erneut aus, !! den letzten Befehl. Am schnellsten: Ctrl-R drücken und tippen, die Shell sucht rückwärts live mit.

MERKE: Profis tippen lange Befehle nur einmal. Danach: Ctrl-R, drei Buchstaben, Enter.
IM EINSATZ
ich@web-01:~bash
$ history | tail -n 3 41 cd /var/log/nginx 42 tail -f access.log 43 history | tail -n 3 (reverse-i-search)`ta': tail -f access.log

Szenario: Freitag, 17:03 Uhr, das Deploy hängt

Slack-Nachricht vom Kollegen: „Das Deploy auf web-01 hängt, schau mal ins Log, ich muss in die Bahn." Du hast SSH-Zugang und weißt nur: Die Logs liegen irgendwo unter /var/log/app. Was tippst du, in welcher Reihenfolge?

Antwort anzeigen

Erst orientieren, dann das neueste Log finden, dann live zuschauen:

ich@web-01:/var/log/appbash
$ cd /var/log/app && ls -lt | head -n 3 -rw-r--r-- 1 deploy deploy 48K 22. Aug 17:02 deploy.log -rw-r--r-- 1 app app 2,1M 22. Aug 17:01 app.log -rw-r--r-- 1 app app 890K 21. Aug 23:59 app.log.1 $ tail -f deploy.log 17:02:41 pulling image registry/app:v2.4.1 … 17:02:58 ERROR healthcheck failed (3/3): port 8080 not responding 17:02:59 rollback started

ls -lt sortiert nach Änderungszeit, das zuletzt beschriebene Log steht oben. head -n 3 zeigt nur die obersten drei Zeilen. Danach hängst du dich mit tail -f live dran und siehst den Healthcheck-Fehler sofort.

MERKE: Nicht raten, welches Log das richtige ist: ls -lt | head zeigt dir, wo gerade wirklich geschrieben wird.

02 · Dateien & Verzeichnisse

Anlegen, kopieren, verschieben, löschen: klingt banal, ist aber der Bereich, in dem die teuersten Fehler passieren. Ein falsches rm und der Feierabend fällt aus. Hier lernst du die Handgriffe, und die Leitplanken dazu.

mkdir

Verzeichnis anlegen.
SYNTAX
mkdir [-p] verzeichnis

-p legt verschachtelte Pfade in einem Rutsch an und meckert nicht, wenn es Teile schon gibt. Ohne -p bricht mkdir a/b/c ab, sobald a fehlt.

IM EINSATZ
ich@web-01:~bash
$ mkdir -p projekt/src/tests $ ls -R projekt projekt: src projekt/src: tests

touch

Leere Datei erstellen.
SYNTAX
touch datei

Existiert die Datei nicht, wird sie leer angelegt. Existiert sie, wird nur ihr Änderungszeitstempel auf jetzt gesetzt. Daher der Name.

IM EINSATZ
ich@web-01:~bash
$ touch TODO.md && ls -l TODO.md -rw-r--r-- 1 ich ich 0 22. Aug 10:02 TODO.md

cp

Kopieren.
SYNTAX
cp [-r] quelle ziel

Für Verzeichnisse brauchst du -r (rekursiv), sonst verweigert cp. Vor riskanten Änderungen an einer Config ist cp datei datei.bak der billigste Lebensretter überhaupt.

MERKE: Erst cp nginx.conf nginx.conf.bak, dann editieren. Immer.
IM EINSATZ
ich@web-01:/etc/nginxbash
$ cp nginx.conf nginx.conf.bak $ cp -r sites-enabled ~/backup/ $ ls ~/backup sites-enabled

mv

Verschieben und Umbenennen.
SYNTAX
mv quelle ziel

Umbenennen ist unter Linux nur Verschieben auf einen neuen Namen. mv braucht kein -r, Verzeichnisse nimmt es einfach mit.

IM EINSATZ
ich@web-01:~bash
$ mv entwurf.md README.md $ mv README.md projekt/ $ ls projekt README.md src

rm

Löschen. Endgültig.
SYNTAX
rm [-r] [-i] datei

-r löscht Verzeichnisse rekursiv, -i fragt vor jeder Datei nach, -f unterdrückt jede Rückfrage. Es gibt keinen Papierkorb: Was rm nimmt, ist weg.

VORSICHT: rm -rf plus falscher Pfad ist der klassischste anzunehmende Unfall. Vor jedem rm -rf: einmal durchatmen und den Pfad lesen. Profis testen vorher mit ls, was das Muster wirklich trifft.
IM EINSATZ
ich@web-01:~bash
$ ls tmp/*.log tmp/debug.log tmp/trace.log $ rm tmp/*.log $ rm -r tmp $ rm -rf / rm: gefährliche Operation verweigert (--no-preserve-root)

ln -s

Verknüpfung anlegen.
SYNTAX
ln -s ziel linkname

Ein Symlink ist ein Wegweiser auf eine andere Datei oder ein Verzeichnis. Deployments nutzen das ständig: current zeigt auf die aktive Version, ein Release-Wechsel ist nur ein neuer Link.

MERKE: Reihenfolge wie bei cp: erst das echte Ziel, dann der Name des Links.
IM EINSATZ
deploy@web-01:/srv/appbash
$ ln -s releases/v2.4.1 current $ ls -l current lrwxrwxrwx 1 deploy deploy 15 22. Aug 11:20 current -> releases/v2.4.1

find

Dateien aufspüren.
SYNTAX
find startpfad [kriterien]

Die wichtigsten Kriterien: -name "*.log" (Muster), -type f (nur Dateien) oder -type d (nur Verzeichnisse), -size +100M (größer als), -mtime -1 (in den letzten 24h geändert). Mit -exec befehl {} \; führst du pro Treffer einen Befehl aus.

IM EINSATZ
ich@web-01:~bash
$ find /var/log -name "*.log" -size +100M /var/log/app/app.log /var/log/nginx/access.log $ find ~ -type f -mtime -1 | head -n 2 /home/ich/notizen.txt /home/ich/logs/app.log

du

Wer frisst den Platz?
SYNTAX
du [-sh] [pfad]

-s fasst pro Argument zusammen, -h macht die Größen lesbar. Die Kombi du -sh * | sort -rh | head ist DER Handgriff bei voller Platte: größte Brocken zuerst.

IM EINSATZ
ich@web-01:/varbash
$ du -sh * 2>/dev/null | sort -rh | head -n 4 2,3G log 812M lib 340M cache 12M spool

Szenario: „Disk usage 94%" um 6:50 Uhr

Das Monitoring weckt dich: Auf web-01 sind nur noch 6 Prozent Platz frei. Du willst in unter einer Minute wissen: Welches Verzeichnis frisst den Platz, und welche Datei genau? Wie gehst du vor?

Antwort anzeigen

Vom Groben ins Feine: erst die Partition, dann das Verzeichnis, dann die Datei.

ich@web-01:~bash
$ df -h / Dateisystem Größe Benutzt Verf. Verw% Eingehängt auf /dev/sda1 40G 37G 2,4G 94% / $ du -sh /var/* 2>/dev/null | sort -rh | head -n 3 28G /var/log 812M /var/lib 340M /var/cache $ find /var/log -size +1G /var/log/app/debug.log

Täter gefunden: ein 27-GB-Debug-Log, das jemand im Fehlereifer angelassen hat. Jetzt nicht einfach löschen, sondern erst klären, ob es noch beschrieben wird, dann leeren statt löschen (truncate -s 0), sonst hält der Prozess die Datei offen und der Platz bleibt belegt.

MERKE: df sagt dir, DASS es eng ist. du sagt dir, WO. find -size sagt dir, WER.

03 · Text-Kung-Fu

Unter Linux ist fast alles Text: Logs, Configs, CSV-Exporte, Prozesslisten. Wer Text filtern, schneiden und zählen kann, beantwortet Fragen in Sekunden, für die andere ein Dashboard bauen. Diese acht Befehle sind deine Klingen.

grep

Zeilen finden, die ein Muster enthalten.
SYNTAX
grep [-inrv] muster [datei…]

-i ignoriert Groß-/Kleinschreibung, -n zeigt Zeilennummern, -r durchsucht ganze Verzeichnisse, -v zeigt alle Zeilen, die das Muster NICHT enthalten. Mit -E nutzt du reguläre Ausdrücke wie "ERROR|WARN".

MERKE: grep ist der meistgetippte Filter der Welt. Wenn du nur einen Befehl aus diesem Modul mitnimmst: diesen.
IM EINSATZ
ich@web-01:~/logsbash
$ grep -n "ERROR" app.log 7:2026-08-22 09:41:09 ERROR db timeout after 5000ms 19:2026-08-22 10:02:33 ERROR queue full, dropping job $ grep -c "ERROR" app.log 2

sed

Suchen und Ersetzen im Strom.
SYNTAX
sed 's/alt/neu/g' datei

Das s heißt substitute, das g ersetzt alle Treffer pro Zeile statt nur den ersten. Ohne weitere Optionen schreibt sed nur auf die Ausgabe, die Datei bleibt unberührt. -i ändert die Datei direkt, dann vorher ein Backup machen (Modul 02 lässt grüßen).

IM EINSATZ
ich@web-01:~bash
$ cat env.conf API_URL=https://staging.example.de $ sed 's/staging/produktion/' env.conf API_URL=https://produktion.example.de

awk

Spalten herausgreifen.
SYNTAX
awk '{print $N}' datei

awk zerlegt jede Zeile an Leerzeichen in Spalten: $1 ist die erste, $NF die letzte. awk kann eine ganze Programmiersprache sein, aber '{print $N}' deckt schon die Hälfte aller Alltagsfälle ab.

IM EINSATZ
ich@web-01:/var/log/nginxbash
$ head -n 2 access.log 203.0.113.7 - - [22/Aug/2026:10:01:44] "GET /api/health" 200 198.51.100.23 - - [22/Aug/2026:10:01:45] "POST /api/login" 401 $ awk '{print $1}' access.log | head -n 2 203.0.113.7 198.51.100.23

cut

Felder mit festem Trenner schneiden.
SYNTAX
cut -d trenner -f feld datei

Wie awk, aber für Dateien mit festem Trennzeichen (Doppelpunkt, Semikolon, Komma). -d: setzt den Trenner, -f1 wählt das erste Feld, -f1,3 mehrere.

IM EINSATZ
ich@web-01:~bash
$ cut -d: -f1 /etc/passwd | head -n 4 root daemon ich deploy

sort

Zeilen sortieren.
SYNTAX
sort [-nrh] [-k feld] datei

-n sortiert numerisch (sonst käme 10 vor 9), -r dreht die Reihenfolge um, -h versteht menschliche Größen wie 2,3G und 812M. -k2 sortiert nach der zweiten Spalte.

IM EINSATZ
ich@web-01:~bash
$ du -sh /var/* 2>/dev/null | sort -rh | head -n 3 2,3G /var/log 812M /var/lib 340M /var/cache

uniq

Duplikate zählen.
SYNTAX
uniq [-c] datei

uniq entfernt doppelte Zeilen, aber nur wenn sie direkt untereinander stehen. Deshalb steht davor fast immer ein sort. -c stellt jeder Zeile ihren Zähler voran: fertig ist die Häufigkeitsstatistik.

MERKE: sort | uniq -c | sort -rn ist das Statistik-Dreigespann: gruppieren, zählen, Top-Liste.
IM EINSATZ
ich@web-01:~bash
$ awk '{print $9}' access.log | sort | uniq -c | sort -rn 1842 200 312 404 89 500 41 301

wc

Zeilen und Wörter zählen.
SYNTAX
wc [-lwc] datei

-l zählt Zeilen und ist die mit Abstand häufigste Form: Wie viele Fehler? Wie viele Deployments heute? Ans Ende einer Pipe gehängt beantwortet wc -l jede „Wie viele…?"-Frage.

IM EINSATZ
ich@web-01:~/logsbash
$ grep "ERROR" app.log | wc -l 2 $ wc -l app.log 20 app.log

diff

Was ist anders?
SYNTAX
diff [-u] datei1 datei2

-u zeigt das gewohnte Patch-Format mit Kontext, wie in Git. Der Klassiker: die laufende Config gegen das Backup vergleichen, bevor du einen Neustart wagst. Keine Ausgabe heißt: identisch.

IM EINSATZ
ich@web-01:/etc/nginxbash
$ diff -u nginx.conf.bak nginx.conf --- nginx.conf.bak +++ nginx.conf @@ -12,1 +12,1 @@ -worker_connections 1024; +worker_connections 4096;

Szenario: Wer hämmert auf die API?

Die API ist träge, das Team vermutet einen Scraper. Vor dir liegt das nginx-access.log mit 1,2 Millionen Zeilen. Frage ans Log: Welche fünf IP-Adressen rufen am häufigsten auf? Ein Einzeiler reicht.

Antwort anzeigen
ich@web-01:/var/log/nginxbash
$ awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -n 5 411208 203.0.113.7 8317 198.51.100.23 5044 192.0.2.115 4890 198.51.100.8 3771 203.0.113.99

Lies die Pipe von links nach rechts wie einen Satz: awk greift die IP-Spalte, sort gruppiert gleiche IPs nebeneinander, uniq -c zählt jede Gruppe, sort -rn stellt die größten Zähler nach oben, head zeigt die Top 5. Eine IP mit 411.000 Aufrufen bei 1,2 Millionen Zeilen: Da ist dein Scraper.

MERKE: uniq zählt nur direkt benachbarte Duplikate. Ohne sort davor ist die Statistik falsch, und zwar leise falsch.

04 · Pipes & Redirection

Einzelne Befehle kann jeder. Die eigentliche Superkraft der Shell ist das Verketten: kleine Werkzeuge, die ihre Ausgabe aneinander weiterreichen. Wer Pipes liest und schreibt, baut aus grep, sort und wc in einer Zeile Auswertungen, für die andere ein Python-Script anfangen.

|

Ausgabe wird Eingabe.
SYNTAX
befehl1 | befehl2 [| befehl3 …]

Die Pipe reicht die Ausgabe des linken Befehls als Eingabe an den rechten weiter. Beim Lesen denkst du an jedem | einfach „… und damit dann". Jedes Werkzeug macht eine Sache, die Pipe macht daraus eine Kette.

MERKE: Pipes baust du von links nach rechts und prüfst nach jedem Schritt. Erst wenn grep das Richtige zeigt, hängst du | wc -l dran.
IM EINSATZ
ich@web-01:~bash
$ grep ERROR logs/app.log | wc -l 3 $ cut -d' ' -f3 logs/app.log | sort | uniq -c | sort -rn 14 INFO 3 WARN 3 ERROR

> und >>

Ausgabe in eine Datei.
SYNTAX
befehl > datei · befehl >> datei

> schreibt die Ausgabe in eine Datei statt aufs Terminal, >> hängt sie ans Ende an. Damit erzeugst du Reports, sammelst Ergebnisse über mehrere Läufe oder baust dir schnell eine Testdatei aus echo.

ACHTUNG: > überschreibt kommentarlos. Ein vertipptes > statt >> und der alte Inhalt ist weg. Keine Rückfrage, kein Papierkorb.
IM EINSATZ
ich@web-01:~bash
$ grep ERROR logs/app.log > fehler.txt $ wc -l fehler.txt 3 fehler.txt $ echo "geprüft: 22.08. 17:41" >> fehler.txt

2>&1

Auch die Fehler einfangen.
SYNTAX
befehl > datei 2>&1 · befehl 2>/dev/null

Programme haben zwei Ausgabekanäle: stdout (Kanal 1) für Ergebnisse, stderr (Kanal 2) für Fehler. > fängt nur Kanal 1. Deshalb stehen Fehlermeldungen trotz Umleitung weiter auf dem Bildschirm. 2>&1 schickt Kanal 2 dorthin, wo Kanal 1 gerade hingeht, 2>/dev/null wirft Fehler weg.

MERKE: Die Reihenfolge zählt: erst > datei, dann 2>&1. Andersherum landet stderr doch wieder auf dem Terminal.
IM EINSATZ
ich@web-01:~bash
$ find /var -name "*.log" > logs.txt find: '/var/lib/private': Keine Berechtigung ↑ trotz > noch da: kam über stderr $ find /var -name "*.log" > logs.txt 2>&1 (still: jetzt steht auch der Fehler in der Datei)

tee

Sehen und speichern gleichzeitig.
SYNTAX
befehl | tee [-a] datei

tee ist das T-Stück im Rohr: Die Daten fließen weiter Richtung Terminal und landen gleichzeitig in der Datei. -a hängt an statt zu überschreiben. Berühmter Nebenjob: echo '…' | sudo tee /etc/datei, um mit Root-Rechten in eine Datei zu schreiben.

MERKE: sudo befehl > datei scheitert an Root-Dateien, weil deine Shell (ohne sudo) die Datei öffnet. | sudo tee datei löst genau das.
IM EINSATZ
ich@web-01:~bash
$ grep WARN logs/app.log | tee warnungen.txt 2026-08-22 09:52:00 WARN queue length 812 2026-08-22 10:16:20 WARN slow query 1810ms: SELECT * FROM orders 2026-08-22 12:10:44 WARN speicher bei 78% … und warnungen.txt hat denselben Inhalt

xargs

Zeilen werden Argumente.
SYNTAX
befehl | xargs [befehl2]

Eine Pipe füttert die Standardeingabe, aber viele Befehle wollen Argumente: rm liest keine Dateiliste von stdin. xargs übersetzt, es nimmt die Zeilen aus der Pipe und hängt sie als Argumente hinten an. find … | xargs … ist das Traumpaar.

MERKE: Bei Dateinamen mit Leerzeichen: find -print0 | xargs -0. Sonst zerlegt xargs einen Namen in zwei Argumente.
IM EINSATZ
ich@web-01:~bash
$ find . -name "*.tmp" ./cache/a.tmp ./cache/b.tmp $ find . -name "*.tmp" | xargs rm $ find . -name "*.tmp" (leer, alle weg)

Szenario: Der 17:45-Uhr-Report

Kurz vor Feierabend fragt dein Teamlead: „Wie viele INFO, WARN und ERROR hatten wir heute in logs/app.log? Häng die Auswertung als Datei ans Ticket, aber sag mir die Zahlen gleich hier." Eine Zeile, beides erledigt. Wie sieht sie aus?

Antwort anzeigen

Level-Spalte herausschneiden, sortieren, zählen, absteigend ordnen, und mit tee gleichzeitig anzeigen und speichern:

ich@web-01:~bash
$ cut -d' ' -f3 logs/app.log | sort | uniq -c | sort -rn | tee report.txt 14 INFO 3 WARN 3 ERROR

Von links gelesen: cut nimmt die dritte Spalte (das Level), sort stellt gleiche Werte nebeneinander, uniq -c zählt sie, sort -rn ordnet nach Häufigkeit, tee schreibt das Ergebnis in report.txt und zeigt es trotzdem an. Die Zeile funktioniert 1:1 im Übungs-Terminal unten.

MERKE: sort | uniq -c | sort -rn ist die Standard-Kette für jede „Was kommt am häufigsten vor?"-Frage. Sie lohnt sich als Muskelgedächtnis.

05 · Rechte & sudo

„Permission denied" ist die häufigste Fehlermeldung deiner Linux-Laufbahn. Wer das Rechtesystem einmal verstanden hat, liest ls -l wie einen Satz: Wer darf hier was, und wer sagt das? Dazu kommt sudo, der Generalschlüssel mit Protokollpflicht.

chmod

Wer darf was?
SYNTAX
chmod [-R] modus datei

Rechte gibt es dreimal: Besitzer (u), Gruppe (g), Rest der Welt (o). Jede Stelle ist die Summe aus Lesen=4, Schreiben=2, Ausführen=1: 755 heißt rwxr-xr-x, 644 heißt rw-r--r--. Symbolisch geht es auch: chmod +x script.sh macht ausführbar, -R wirkt rekursiv.

MERKE: Die Oktal-Mathe: 7=4+2+1=rwx, 6=rw-, 5=r-x, 4=r--, 0=nichts. Faustregel: 755 für Verzeichnisse und Scripts, 644 für Dateien, 600 für Geheimnisse.
IM EINSATZ
ich@web-01:~bash
$ ls -l deploy.sh -rw-r--r-- 1 ich ich 412 22. Aug 10:00 deploy.sh $ chmod +x deploy.sh && ls -l deploy.sh -rwxr-xr-x 1 ich ich 412 22. Aug 10:00 deploy.sh

chown & chgrp

Wem gehört das?
SYNTAX
chown [-R] besitzer[:gruppe] pfad

Jede Datei hat einen Besitzer und eine Gruppe. Der Klassiker: Nach einem Deploy als root gehört plötzlich alles root, und der Webserver darf seine eigenen Dateien nicht mehr lesen. chown -R www-data:www-data stellt das richtig. chgrp ändert nur die Gruppe, chown darf nur root.

MERKE: Steht in ls -l statt eines Namens nur eine Zahl (1003 1003), wurde der Besitzer-User gelöscht. Die Datei zeigt seine verwaiste UID.
IM EINSATZ
ich@web-01:~bash
$ ls -l /var/www/app/index.html -rw-r--r-- 1 root root 1,2K 22. Aug 09:58 index.html $ sudo chown -R www-data:www-data /var/www/app $ ls -l /var/www/app/index.html -rw-r--r-- 1 www-data www-data 1,2K 22. Aug 09:58 index.html

umask

Warum ist die Datei 644?
SYNTAX
umask · umask 027

Die umask ist die Schablone, die beim Anlegen von den Maximalrechten abgezogen wird: 666 für Dateien, 777 für Verzeichnisse. Standard 022 ergibt darum 644 und 755. Gehärtete Umgebungen setzen 027: Die Gruppe darf lesen, alle anderen gar nichts.

MERKE: Deshalb sind frische Dateien nie ausführbar: Das x-Bit steckt gar nicht in den 666. Ausführbar wird eine Datei immer erst per chmod +x.
IM EINSATZ
ich@web-01:~bash
$ umask 0022 $ touch neu.txt && ls -l neu.txt -rw-r--r-- 1 ich ich 0 22. Aug 10:01 neu.txt 666 minus 022 = 644

sudo

Der Generalschlüssel, mit Protokoll.
SYNTAX
sudo befehl · sudo -l · sudo !!

sudo führt genau einen Befehl als root aus und protokolliert, wer wann was durfte. sudo -l zeigt, welche Befehle dir erlaubt sind. Konfiguriert wird das in /etc/sudoers, und zwar ausschließlich mit visudo: Das prüft die Syntax vor dem Speichern. Ein Tippfehler dort sperrt sonst alle aus.

MERKE: sudo !! wiederholt den letzten Befehl mit Root-Rechten. Praktisch, wenn die Fehlermeldung gerade eben „Permission denied" war, und er wirklich Root braucht.
IM EINSATZ
ich@web-01:~bash
$ sudo -l Benutzer ich darf auf web-01 folgende Befehle ausführen: (ALL : ALL) ALL $ sudo systemctl restart nginx [sudo] Passwort für ich:

su & sudo -i

Ganz jemand anderes werden.
SYNTAX
sudo -i · su - benutzer

Für einzelne Befehle reicht sudo. Willst du länger als root arbeiten, öffnet sudo -i eine echte Root-Shell samt Roots Umgebung. su - deploy wechselt zum Service-User, etwa um ein Problem mit dessen Rechten und Umgebung nachzustellen.

ACHTUNG: In einer Root-Shell gibt es kein Sicherheitsnetz: keine Rückfrage, kein Papierkorb, jedes rm wirkt sofort und überall. Reingehen, erledigen, mit exit wieder raus.
IM EINSATZ
ich@web-01:~bash
$ sudo -i root@web-01:~# whoami root root@web-01:~# exit $

Szenario: 755 und trotzdem Permission denied

Dein Backup-Script liegt in /tmp, hat 755, gehört dir, und trotzdem: bash: ./backup.sh: Permission denied. Noch ein chmod +x ändert nichts. Was prüfst du als Nächstes?

Antwort anzeigen

Nicht die Rechte, sondern das Dateisystem: mount verrät, ob die Partition mit noexec eingehängt ist. Auf gehärteten Systemen ist genau das für /tmp Standard.

ich@web-01:/tmpbash
$ ls -l backup.sh -rwxr-xr-x 1 ich ich 812 22. Aug 10:12 backup.sh $ ./backup.sh bash: ./backup.sh: Permission denied $ mount | grep /tmp tmpfs on /tmp type tmpfs (rw,nosuid,nodev,noexec) $ mv backup.sh ~/bin/ && ~/bin/backup.sh backup gestartet …

Die Rechte-Bits sind nur die halbe Wahrheit: Auch der Mount hat ein Wörtchen mitzureden. noexec verbietet Ausführen auf der ganzen Partition, egal was chmod sagt. bash backup.sh würde es auch umgehen (bash liest die Datei nur), sauberer ist ein Verzeichnis ohne noexec.

MERKE: Die Prüf-Reihenfolge bei Permission denied: ls -l auf die Datei, dann die Verzeichnisse darüber, dann mount. Erst wenn alle drei sauber sind, wird es exotisch.

06 · Prozesse

Auf jedem Server laufen hunderte Prozesse, und meistens ist es genau einer, der dir den Tag versaut. CPU auf 100 %, API zäh, Monitoring rot: Hier lernst du, den Schuldigen zu finden, ihn sauber zu beenden und Dienste im Griff zu behalten.

ps

Wer läuft hier alles?
SYNTAX
ps aux [--sort=-%cpu|-%mem]

ps aux zeigt alle Prozesse aller Nutzer: PID, CPU- und Speicheranteil, Startzeit, Befehl. Mit --sort=-%cpu oder -%mem stehen die größten Verbraucher oben, mit | grep suchst du gezielt nach einem Namen.

MERKE: Die PID ist der Hebel für alles Weitere: kill, /proc/PID, strace. Erst identifizieren, dann eingreifen.
IM EINSATZ
ich@web-01:~bash
$ ps aux --sort=-%cpu | head -n 3 USER PID %CPU %MEM COMMAND ich 4711 397 38,2 python export.py ich 2114 1,2 2,3 python app.py

top & htop

Prozesse live beobachten.
SYNTAX
top · htop

top ist das Live-Dashboard: Lastdurchschnitt, CPU, Speicher und die Prozessliste, sekündlich aktualisiert. Drinnen sortiert M nach Speicher, P nach CPU, k beendet eine PID, q geht raus. htop kann dasselbe hübscher, mit Scrollen, Suche und F-Tasten.

MERKE: Der load average sind drei Zahlen: 1, 5 und 15 Minuten. Dauerhaft über der Kern-Anzahl heißt: Prozesse stehen Schlange.
IM EINSATZ
ich@web-01:~top
top - 10:02:11 up 12 days, load average: 4,07 1,89 0,92 %CPU(s): 91,2 us, 6,1 sy, 0,3 id PID USER %CPU %MEM COMMAND 4711 ich 397 38,2 python export.py 2114 ich 1,2 2,3 python app.py 4 Kerne, Load 4,07: es staut sich gerade

kill & killall

Prozesse sauber beenden.
SYNTAX
kill [-9] pid · killall name

kill schickt Signale. Ohne Angabe SIGTERM (15): die höfliche Bitte, sich geordnet zu beenden, Verbindungen zu schließen, aufzuräumen. kill -9 (SIGKILL) reißt den Prozess sofort weg, ohne jedes Aufräumen. killall name trifft alle Prozesse mit diesem Namen.

ACHTUNG: kill -9 ist der Vorschlaghammer: keine Cleanup-Handler, Locks und Temp-Dateien bleiben liegen. Erst normal killen, ein paar Sekunden warten, dann eskalieren.
IM EINSATZ
ich@web-01:~bash
$ kill 4711 $ ps aux | grep export (leer, sauber beendet) $ kill -9 4711 ↑ nur, wenn er nach TERM nicht hören will

&, jobs, fg, bg

Vordergrund, Hintergrund.
SYNTAX
befehl & · Ctrl-Z · jobs · fg %1 · bg %1

Ein & am Ende startet den Befehl im Hintergrund, dein Terminal bleibt frei. Läuft schon etwas im Vordergrund: Ctrl-Z hält es an, bg lässt es hinten weiterlaufen, fg holt es zurück. jobs zeigt die Liste mit den %-Nummern.

MERKE: Ctrl-Z ist nicht Ctrl-C: Z pausiert nur (der Prozess lebt weiter), C bricht ab.
IM EINSATZ
ich@web-01:~bash
$ ./backup.sh & [1] 5023 $ jobs [1]+ Läuft ./backup.sh & $ fg %1 ./backup.sh

nohup & nice

Weiterlaufen, wenn du gehst.
SYNTAX
nohup befehl & · nice -n 10 befehl

Schließt du das SSH-Fenster, sterben deine Prozesse mit. nohup macht sie immun gegen das Auflegen (HUP-Signal), die Ausgabe landet in nohup.out. nice -n 10 startet einen Prozess mit niedrigerer Priorität: gut für dicke Batch-Jobs, die niemanden ausbremsen sollen.

MERKE: Der Dreiklang für „läuft auch ohne mich": nohup ./job.sh &, später mit tail -f nohup.out reinschauen. Für ernsthafte Dienste nimmst du systemd, nächste Karte.
IM EINSATZ
ich@web-01:~bash
$ nohup ./export.sh & [1] 5107 nohup: Ausgabe wird an 'nohup.out' angehängt $ exit export.sh läuft weiter, morgen früh steht alles in nohup.out

systemctl

Dienste verwalten.
SYNTAX
systemctl status|start|restart|enable dienst

Echte Dienste (nginx, Postgres, deine App) laufen unter systemd. status zeigt Zustand, PID und die letzten Log-Zeilen, restart startet neu, enable sorgt für Autostart beim Boot. systemctl --failed listet auf, was gerade kaputt ist.

MERKE: enable ist nicht start: enable wirkt erst ab dem nächsten Boot, start sofort. Beides in einem: systemctl enable --now dienst.
IM EINSATZ
ich@web-01:~bash
$ systemctl status nginx ● nginx.service - A high performance web server Active: active (running) since Sat 2026-08-22 09:14:02 Main PID: 1337 (nginx) $ sudo systemctl restart nginx (kein Output ist bei systemd ein gutes Zeichen)

Szenario: Der Server glüht

Monitoring-Alarm: CPU auf web-01 seit 20 Minuten bei 100 %, die API antwortet zäh. Du loggst dich ein. Wie findest du den Schuldigen, und wie wirst du ihn los, ohne Kollateralschaden anzurichten?

Antwort anzeigen

Erst identifizieren, dann höflich beenden, erst danach eskalieren:

ich@web-01:~bash
$ ps aux --sort=-%cpu | head -n 3 USER PID %CPU %MEM COMMAND ich 4711 397 38,2 python export.py ich 2114 1,2 2,3 python app.py $ kill 4711 $ ps aux | grep 4711 (leer, er ist weg)

397 % CPU heißt: fast vier Kerne nur für export.py. Nach dem TERM ein paar Sekunden warten und nachprüfen, erst wenn er dann noch lebt, kill -9 4711. Danach die eigentliche Arbeit: Warum lief der Export Amok? Cron-Job, Endlosschleife, zu große Datenmenge? Und falls der Prozess ein systemd-Dienst ist, lieber systemctl restart statt kill, sonst startet systemd ihn sowieso gleich neu.

MERKE: Die Reihenfolge: identifizieren (ps/top), sauber beenden (kill), eskalieren (-9), Ursache klären. Wer mit -9 anfängt, macht die Forensik kaputt.

07 · Memory & Performance

Speicher zu 95 % belegt? Bloß nicht sofort neu starten. Linux nutzt freien RAM absichtlich als Cache, und wer das nicht weiß, jagt Geister. Hier lernst du, echte Speichernot von gesundem Caching zu unterscheiden, und den OOM-Killer zu überführen, wenn nachts eine App verschwindet.

free

Ist der Speicher wirklich voll?
SYNTAX
free -h

free -h zeigt gesamt, benutzt, frei, und die entscheidende Spalte: verfügbar. Linux parkt ungenutzten RAM als Datei-Cache und gibt ihn sofort wieder her, sobald ein Programm ihn braucht. Ein „voller" Speicher ist deshalb meistens ein gesunder Speicher.

MERKE: Free heißt „gerade leer", Available heißt „sofort nutzbar". Bewerte immer available. Hohes used mit hohem available ist kein Problem, sondern Cache bei der Arbeit.
IM EINSATZ
ich@web-01:~bash
$ free -h gesamt benutzt frei verfügbar Speicher: 15Gi 6,2Gi 1,1Gi 8,3Gi Swap: 4,0Gi 0,2Gi 3,8Gi nur 1,1Gi „frei", aber 8,3Gi verfügbar: alles gut

ps aux --sort=-%mem

Wer frisst den RAM?
SYNTAX
ps aux --sort=-%mem | head

Wenn available wirklich schrumpft, willst du den Verursacher: Nach %MEM sortiert stehen die Speicherfresser oben, die Spalte RSS zeigt die echten Bytes im RAM. Ein Prozess, dessen RSS stetig wächst und nie wieder fällt, ist dein Leak-Kandidat.

MERKE: Einmal messen ist ein Foto, zweimal messen ist ein Film. Miss im Abstand von Minuten, Wachstum erkennst du nur im Vergleich.
IM EINSATZ
ich@web-01:~bash
$ ps aux --sort=-%mem | head -n 3 USER PID %CPU %MEM RSS COMMAND ich 4711 2,1 38,2 6,1g python export.py root 612 0,0 4,8 780m dockerd

vmstat

Atmet das System, oder röchelt es?
SYNTAX
vmstat 1

vmstat 1 druckt jede Sekunde eine Zustandszeile. Die wichtigsten Spalten: r (Prozesse, die auf CPU warten), si/so (Swap rein/raus), wa (CPU wartet auf die Platte). Steht so dauerhaft über 0, lagert das System aktiv aus, und jetzt wird es wirklich eng.

MERKE: Ein einzelner Messwert lügt. Lass vmstat laufen und lies den Trend: Erst der Verlauf macht aus Zahlen eine Diagnose.
IM EINSATZ
ich@web-01:~bash
$ vmstat 1 procs -----------memory---------- ---swap-- ----cpu---- r b swpd frei buff cache si so us sy id wa 1 0 204800 1153434 88320 6653952 0 0 7 2 90 1 8 1 204800 212992 12288 1053440 512 2048 21 9 31 39 Zeile 2: so>0 und Cache bricht ein, das System swappt

/proc/meminfo & swapon

Die Quelle der Wahrheit.
SYNTAX
cat /proc/meminfo · swapon --show

free ist nur die hübsche Ansicht von /proc/meminfo. Dort stehen die Rohwerte, allen voran MemAvailable: die Kernel-Schätzung, wie viel RAM ohne Swapping nutzbar ist. swapon --show zeigt die aktiven Swap-Bereiche, ob Datei oder Partition.

MERKE: Monitoring und Skripte lesen MemAvailable direkt aus /proc/meminfo. Und /proc ist keine Platte, sondern der Kernel selbst, der dir in Dateiform Auskunft gibt.
IM EINSATZ
ich@web-01:~bash
$ grep MemAvailable /proc/meminfo MemAvailable: 8704512 kB $ swapon --show NAME TYPE SIZE BENUTZT PRIO /swapfile file 4,0G 204,8M -2

OOM-Forensik

War es wirklich der OOM-Killer?
SYNTAX
dmesg | grep -i oom · journalctl -k

Geht der Speicher wirklich aus, wählt der Kernel ein Opfer und beendet es: der OOM-Killer. Deine App ist nachts „einfach weg"? Das steht im Kernel-Log. dmesg | grep -i oom (oder journalctl -k) liefert den Beweis samt Prozessname, PID und Speicherbedarf zum Tatzeitpunkt.

MERKE: „Killed process" im Kernel-Log ist ein Befund, keine Lösung. Danach klärst du: Leckt die App, oder ist die Maschine zu klein? Neustart ohne Log-Blick heißt: nächste Nacht, gleiches Spiel.
IM EINSATZ
ich@web-01:~bash
$ dmesg | grep -i "out of memory" [81422.447019] Out of memory: Killed process 4711 (python) total-vm:7864320kB, anon-rss:6291456kB Täter: der Kernel. Opfer: export.py. Motiv: 6 GB anon-rss.

Szenario: Die App war heute Nacht einfach weg

03:12 Uhr Monitoring-Alarm, die App war down, der Kollege aus der Frühschicht hat sie um sechs neu gestartet. Jetzt sollst du klären: Absturz, Deploy-Fehler, oder hat der OOM-Killer zugeschlagen? Wie beweist du es, und was prüfst du danach?

Antwort anzeigen

Der Kronzeuge ist das Kernel-Log, danach schaust du dir die aktuelle Lage an:

ich@web-01:~bash
$ dmesg | grep -i oom [81420.001322] python invoked oom-killer: gfp_mask=0x140cca, order=0 [81422.447019] Out of memory: Killed process 4711 (python) total-vm:7864320kB, anon-rss:6291456kB $ free -h Speicher: 15Gi 6,2Gi 1,1Gi 8,3Gi $ ps aux --sort=-%mem | head -n 3 … und in 10 Minuten nochmal, für den Vergleich

Das Log beantwortet die Schuldfrage eindeutig: Der Kernel hat PID 4711 beendet, und anon-rss:6291456kB verrät, dass die App zum Tatzeitpunkt gut 6 GB im RAM hielt. Danach die Anschlussfrage: Wächst der Speicher der neu gestarteten App schon wieder stetig (Leak), oder war es eine einmalige Lastspitze? Das klärt ps aux --sort=-%mem im Zeitvergleich. Je nach Befund: Leak fixen, Memory-Limit setzen, oder ehrlich sein und die Maschine vergrößern.

MERKE: Der Dreisatz der Memory-Forensik: dmesg (was ist passiert), free und /proc/meminfo (wie ist die Lage), ps --sort=-%mem im Zeitvergleich (wohin läuft es). Funktioniert übrigens 1:1 im Übungs-Terminal, da wartet eine Mission darauf.

08 · Disk & Storage

Volle Platten sind der leiseste Ausfall der Welt: Erst schreibt das Log nicht mehr, dann stirbt die Datenbank, und keiner weiß warum. Hier lernst du, Platz zu finden, Übeltäter einzukreisen und Daten sicher zu verpacken und zu verschieben.

df

Wie voll sind die Platten?
SYNTAX
df -h [pfad]

df zeigt pro Dateisystem: Größe, belegt, verfügbar, Einhängepunkt. -h macht die Zahlen lesbar. Mit Pfad-Argument siehst du sofort, auf welcher Partition ein Verzeichnis liegt. df -i zeigt Inodes: Auch die können ausgehen, während noch Platz frei ist (Millionen Mini-Dateien).

MERKE: 100 % auf / ist ein Notfall: Dienste können nicht mehr schreiben und sterben leise. Ab 90 % handeln, nicht ab 100.
IM EINSATZ
ich@web-01:~bash
$ df -h Dateisystem Größe Benutzt Verf. Verw% Eingehängt auf /dev/sda1 40G 37G 1,2G 97% / tmpfs 2,0G 0 2,0G 0% /tmp 97 %: Zeit für Modul-08-Wissen

du

Wo ist der Platz hin?
SYNTAX
du -sh [pfad] · du -sh * | sort -rh

du misst, was Verzeichnisse wirklich belegen: -s fasst zusammen, -h lesbar. Der Alltagsgriff: du -sh * | sort -rh | head zeigt die dicksten Brocken zuerst. Dann steigst du in den größten hinab und wiederholst das Spiel, Ebene für Ebene.

MERKE: df sagt, DASS es eng ist (Dateisystem-Sicht). du sagt, WER es war (Verzeichnis-Sicht). Erst df, dann du.
IM EINSATZ
ich@web-01:/varbash
$ du -sh * 2>/dev/null | sort -rh | head -n 3 31G log 890M lib 120M cache

lsblk & mount

Welche Platten, wo eingehängt?
SYNTAX
lsblk · mount | grep pfad

lsblk zeigt den Hardware-Baum: Platten (sda), Partitionen (sda1), Größen, Einhängepunkte. mount zeigt, was wo mit welchen Optionen eingehängt ist, inklusive der Fallen ro (read-only) und noexec.

MERKE: Bekannt aus Modul 05: Permission denied trotz korrekter Rechte? mount | grep verrät die Mount-Optionen der Partition.
IM EINSATZ
ich@web-01:~bash
$ lsblk NAME SIZE TYP MOUNTPOINT sda 40G disk └─sda1 40G part / $ mount | grep /tmp tmpfs on /tmp type tmpfs (rw,nosuid,nodev,noexec)

tar

Packen und entpacken.
SYNTAX
tar -czf ziel.tar.gz quelle · tar -xzf archiv

Vier Buchstaben, ein System: c create, x extract, t table (nur reinschauen), z gzip, f file (der Dateiname folgt direkt). tar -czf backup.tar.gz projekt packt, -xzf entpackt, -tzf zeigt vorher, was drin ist.

MERKE: Vor dem Entpacken fremder Archive: tar -tzf. Manche explodieren als hundert Einzeldateien ins aktuelle Verzeichnis, die berüchtigte Tarbomb.
IM EINSATZ
ich@web-01:~bash
$ tar -czf projekt.tar.gz projekte/ $ tar -tzf projekt.tar.gz | head -n 3 projekte/ projekte/webapp/ projekte/webapp/app.py

rsync

Kopieren für Erwachsene.
SYNTAX
rsync -avz quelle/ ziel/

rsync überträgt nur, was sich geändert hat: Abgebrochene Läufe setzen fort, wiederholte Läufe sind fast sofort fertig. -a erhält Rechte und Zeitstempel, -v zeigt Dateien, -z komprimiert. Über SSH ist es das Standard-Werkzeug für Deployments und Backups.

ACHTUNG: Der Slash zählt: quelle/ kopiert den Inhalt, quelle ohne Slash das Verzeichnis selbst. Der Unterschied hat schon viele doppelt verschachtelte Backups erzeugt.
IM EINSATZ
ich@web-01:~bash
$ rsync -avz logs/ backup-01:/srv/backup/logs/ sending incremental file list app.log sent 48.213 Bytes empfangen 35 Bytes zweiter Lauf: fertig in Millisekunden, nichts zu tun

Szenario: Platte voll, aber wovon?

Alarm: / ist bei 97 %, die Datenbank verweigert Schreibzugriffe. Du hast fünf Minuten, bevor es eskaliert. Wie findest du heraus, was den Platz frisst, und was tust du zuerst?

Antwort anzeigen

Erst bestätigen (df), dann eingrenzen (du), Ebene für Ebene wie eine Schnitzeljagd:

ich@web-01:~bash
$ df -h / /dev/sda1 40G 37G 1,2G 97% / $ du -sh /var/* 2>/dev/null | sort -rh | head -n 3 31G /var/log 890M /var/lib 120M /var/cache $ du -sh /var/log/* | sort -rh | head -n 2 29G /var/log/app.log.1 1,8G /var/log/app.log

Der Übeltäter ist ein rotiertes, nie gelöschtes Riesen-Log. Soforthilfe: komprimieren oder löschen (gzip /var/log/app.log.1 schafft sofort Luft). Danach die Ursache abstellen: logrotate konfigurieren, damit alte Logs komprimiert und nach X Tagen gelöscht werden, sonst stehst du nächsten Monat wieder hier.

MERKE: Symptom beheben UND Ursache abstellen sind zwei Aufgaben. Wer nur löscht, hat einen Termin mit demselben Problem.

09 · Netzwerk

„Die API ist down!" Ist sie meistens gar nicht: Irgendwo zwischen Prozess, Port, Firewall und DNS klemmt genau eine Schicht. Mit sechs Befehlen grenzt du systematisch ein, welche, und debuggst nie wieder auf gut Glück.

ip

Wer bin ich im Netz?
SYNTAX
ip [-br] a · ip r

ip a zeigt Interfaces und Adressen (-br macht es kompakt), ip r die Routen samt Default-Gateway. Das alte ifconfig ist im Ruhestand. Für die Sicht von außen: curl ifconfig.me verrät deine öffentliche IP.

MERKE: Die erste Frage bei „kein Netz": Hat das Interface überhaupt eine IP, und stimmt die Default-Route? ip a, dann ip r.
IM EINSATZ
ich@web-01:~bash
$ ip -br a lo UNKNOWN 127.0.0.1/8 eth0 UP 10.0.0.12/24 $ ip r default via 10.0.0.1 dev eth0

ping

Bist du da?
SYNTAX
ping -c 4 host

ping schickt Echo-Pakete und misst die Antwortzeit, -c 4 begrenzt auf vier Versuche. Paketverlust und schwankende Zeiten sind die ersten Hinweise auf Netzprobleme. Aber: Manche Server filtern ICMP weg und sind trotzdem kerngesund.

MERKE: „ping geht, curl nicht" ist eine wertvolle Diagnose: Host erreichbar, aber Dienst oder Port haben ein Problem. Das grenzt sauber ein.
IM EINSATZ
ich@web-01:~bash
$ ping -c 2 10.0.0.5 64 Bytes von 10.0.0.5: icmp_seq=1 ttl=64 Zeit=0.42 ms 64 Bytes von 10.0.0.5: icmp_seq=2 ttl=64 Zeit=0.39 ms 2 Pakete übertragen, 2 empfangen, 0% Verlust

ss

Wer lauscht auf welchem Port?
SYNTAX
ss -tlnp

Die vier Buchstaben: t TCP, l nur Lauschende, n Nummern statt Namen, p Prozess dazu. Damit beantwortest du die wichtigste Netzwerkfrage auf jedem Server: Läuft mein Dienst, und auf welcher Adresse? (netstat ist der Vorgänger, ss der Standard.)

MERKE: 127.0.0.1:8080 heißt: nur lokal erreichbar. 0.0.0.0:8080 heißt: von überall. Dieser Unterschied ist die halbe Miete beim „von außen geht nichts"-Debugging.
IM EINSATZ
ich@web-01:~bash
$ ss -tlnp State Local Address:Port Process LISTEN 0.0.0.0:22 sshd LISTEN 0.0.0.0:80 nginx LISTEN 127.0.0.1:8080 python (app.py)

curl

HTTP aus der Shell.
SYNTAX
curl url · curl -I url · curl -v url

curl holt URLs, perfekt zum API-Testen ohne Browser. -I zeigt nur die Antwort-Header (Statuscode!), -v das komplette Gespräch inklusive TLS und Redirects, -s macht es still für Pipes. curl ifconfig.me verrät deine öffentliche IP.

MERKE: Debug-Reihenfolge: curl auf dem Server gegen localhost, dann von außen gegen die IP, dann gegen den Domainnamen. Wo es bricht, wohnt der Fehler.
IM EINSATZ
ich@web-01:~bash
$ curl localhost:8080/api/health {"status": "ok"} $ curl -I localhost:8080 HTTP/1.1 200 OK Content-Type: application/json

dig

Was sagt das DNS?
SYNTAX
dig +short name · dig name @8.8.8.8

dig fragt DNS-Server direkt. +short liefert nur die IP, ideal für Skripte. Mit @8.8.8.8 fragst du gezielt einen anderen Resolver: Unterscheiden sich die Antworten, ist dein DNS oder ein alter Cache das Problem.

MERKE: „Es ist immer DNS" ist nur ein halber Witz. Bei „Dienst nicht erreichbar" gehört dig +short in die ersten drei Befehle.
IM EINSATZ
ich@web-01:~bash
$ dig +short api.example.com 93.184.216.34 $ dig +short api.example.com @8.8.8.8 93.184.216.34 gleiche Antwort: das DNS ist unschuldig, diesmal

ssh & scp

Da rüber, und Dateien mitnehmen.
SYNTAX
ssh user@host ['befehl'] · scp datei user@host:/pfad/

ssh ist deine Standleitung auf fremde Maschinen, mit Schlüsseln statt Passwörtern (ssh-keygen, dann ssh-copy-id). scp kopiert Dateien durch denselben Tunnel, in beide Richtungen. Für große oder wiederholte Kopien: rsync über SSH (Modul 08).

MERKE: ssh user@host 'befehl' führt einen einzelnen Befehl remote aus und kommt zurück. Das ist die Grundlage von fast jeder Automatisierung.
IM EINSATZ
ich@laptop:~bash
$ ssh ich@web-01 'uptime' 10:02:11 up 12 days, load average: 0,42 0,38 0,31 $ scp report.txt ich@web-01:/tmp/ report.txt 100% 412 0,4KB/s 00:00

Szenario: Von außen geht nichts

Deine App antwortet auf dem Server (curl localhost:8080 liefert 200), aber vom Laptop aus: Timeout. DNS stimmt, ping auf den Server geht. Wo suchst du weiter, und was ist die häufigste Ursache?

Antwort anzeigen

Wenn der Dienst lokal antwortet, ist die nächste Frage: An welche Adresse ist er gebunden?

ich@web-01:~bash
$ curl -s localhost:8080/api/health {"status": "ok"} $ ss -tlnp | grep 8080 LISTEN 127.0.0.1:8080 python (app.py)

Da steht die Antwort: Die App bindet nur an 127.0.0.1, das Loopback-Interface. Von außen führt dorthin kein Weg, egal was Firewall und DNS sagen. Fix: in der App-Config an 0.0.0.0 binden, oder sauberer: einen Reverse-Proxy (nginx) davorschalten, der 80/443 nach innen weiterreicht. Die zweithäufigste Ursache wäre die Firewall (ufw status). Die beiden Befehle oben funktionieren übrigens 1:1 im Übungs-Terminal, auch das ist eine Mission.

MERKE: Debugge von innen nach außen: Läuft der Prozess? → Auf welcher Adresse lauscht er (ss -tlnp)? → Firewall? → DNS? Die Antwort steht meistens schon bei ss.

10 · Sysadmin-Alltag

Das letzte Modul macht aus Befehlen einen Beruf: Aufgaben automatisieren, Logs lesen, Systeme pflegen, Nutzer verwalten. Und am Ende richtest du dir die Shell so ein, dass sie sich anfühlt wie dein Werkzeug, nicht wie geliehen.

crontab

Aufgaben nach Stundenplan.
SYNTAX
crontab -e · crontab -l

Fünf Felder, dann der Befehl: Minute Stunde Monatstag Monat Wochentag. 0 3 * * * heißt täglich 03:00, */10 * * * * alle zehn Minuten, 0 9 * * 1-5 werktags um neun. -l zeigt deine Jobs, -e öffnet sie im Editor.

MERKE: Cron kennt weder dein PATH noch dein Home: absolute Pfade verwenden und die Ausgabe umleiten: >> /var/log/job.log 2>&1 (Modul 04 lässt grüßen).
IM EINSATZ
ich@web-01:~bash
$ crontab -l 0 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1 */10 * * * * /usr/local/bin/health-check.sh

journalctl

Das Log-Gedächtnis von systemd.
SYNTAX
journalctl -u dienst [-f] [--since "…"]

journalctl liest das zentrale systemd-Journal. -u nginx filtert auf einen Dienst, -f folgt live (wie tail -f), --since "1 hour ago" und -p err grenzen Zeitraum und Priorität ein. -k zeigt nur Kernel-Meldungen, bekannt aus der OOM-Forensik.

MERKE: Der Dreiklang bei Dienst-Problemen: systemctl status (Zustand jetzt), journalctl -u (die Geschichte), journalctl -u -f (live dabei).
IM EINSATZ
ich@web-01:~bash
$ journalctl -u app --since "03:00" | tail -n 3 Aug 23 03:11:58 web-01 python[2114]: MemoryError Aug 23 03:12:02 web-01 systemd[1]: app.service: Main process exited Aug 23 06:00:41 web-01 systemd[1]: Started app.service.

uname & os-release

Was für eine Kiste ist das?
SYNTAX
uname -a · cat /etc/os-release

Erste Minute auf einer fremden Maschine: uname -a (Kernel, Architektur), cat /etc/os-release (welche Distribution), dazu df -h und free -h für die Reserven. dmesg -T zeigt das Kernel-Log mit lesbaren Zeitstempeln: Hardware, Disk-Fehler, OOM-Kills.

MERKE: uname -a, os-release, df -h, free -h: der 60-Sekunden-Steckbrief jeder fremden Maschine. Erst orientieren, dann anfassen.
IM EINSATZ
ich@web-01:~bash
$ uname -a Linux web-01 6.9.0 #1 SMP PREEMPT x86_64 GNU/Linux $ cat /etc/os-release | head -n 1 PRETTY_NAME="Debian GNU/Linux 13 (trixie)"

apt

Software rein, Software raus.
SYNTAX
apt update · apt upgrade · apt install paket

apt update lädt nur die Paketlisten (was gäbe es Neues?), apt upgrade installiert die Updates dann wirklich. install holt Neues, search sucht, remove entfernt. Auf RHEL/Fedora heißt das Ganze dnf, auf Arch pacman, das Prinzip bleibt gleich.

MERKE: Immer erst update, dann install oder upgrade. Ohne frische Listen installierst du veraltete Versionen oder läufst in 404-Fehler.
IM EINSATZ
ich@web-01:~bash
$ sudo apt update Alle Paketlisten sind aktuell. $ sudo apt install htop htop (3.3.0-4) wird installiert …

id, useradd & usermod

Wer darf hier arbeiten?
SYNTAX
id [user] · useradd -m name · usermod -aG gruppe name

id zeigt, wer du bist und in welchen Gruppen du steckst. sudo useradd -m -s /bin/bash anna legt einen User samt Home an, sudo usermod -aG docker anna nimmt ihn in eine Gruppe auf. Die neue Mitgliedschaft gilt erst nach dem nächsten Login.

ACHTUNG: usermod -G ohne -a ERSETZT alle bisherigen Gruppen des Users. Das kleine -a (append) ist der Unterschied zwischen „hinzufügen" und „aussperren".
IM EINSATZ
ich@web-01:~bash
$ id uid=1000(ich) gid=1000(ich) Gruppen=1000(ich),27(sudo),999(docker) $ sudo usermod -aG docker anna && id anna uid=1001(anna) gid=1001(anna) Gruppen=1001(anna),999(docker)

alias & .bashrc

Die Shell an dich anpassen.
SYNTAX
alias kurz='langer befehl' · ~/.bashrc

Ein alias macht aus deinem Lieblings-Langbefehl einen Kurzbefehl. Dauerhaft wird er in ~/.bashrc (bzw. ~/.zshrc): Die läuft bei jedem Shell-Start. Auch PATH-Erweiterungen und export EDITOR=vim wohnen dort. Nach Änderungen: source ~/.bashrc.

MERKE: Verhält sich ein Befehl auf deiner Maschine anders als überall sonst? type befehl zeigt, ob ein Alias dazwischenfunkt.
IM EINSATZ
ich@web-01:~bash
$ alias ll='ls -lah' $ type ll ll ist ein Alias von `ls -lah'. $ echo "alias ll='ls -lah'" >> ~/.bashrc ab jetzt in jeder neuen Shell dabei

Szenario: Das Backup lief nicht

Dein nächtlicher Cron-Job (0 3 * * * backup.sh) hat heute Nacht nichts geschrieben. Von Hand gestartet läuft backup.sh sauber durch. Wie findest du heraus, was um 03:00 passiert ist, und was ist die klassische Ursache?

Antwort anzeigen

Drei Fragen, drei Befehle: Steht der Job? Hat Cron ihn gestartet? Was hat er gesagt?

ich@web-01:~bash
$ crontab -l 0 3 * * * backup.sh >> /var/log/backup.log 2>&1 $ grep CRON /var/log/syslog | tail -n 1 Aug 23 03:00:01 web-01 CRON[8812]: (ich) CMD (backup.sh >> /var/log/backup.log 2>&1) $ cat /var/log/backup.log /bin/sh: 1: backup.sh: not found

Cron HAT den Job gestartet (das syslog beweist es), aber die Shell fand backup.sh nicht: Cron läuft mit Mini-PATH und nicht in deinem Home. Von Hand klappt es nur, weil DEINE Shell den Pfad kennt. Fix: absoluter Pfad in der Crontab (/home/ich/bin/backup.sh). Und weil die Ausgabe schon umgeleitet war, stand die Fehlermeldung im Log, statt spurlos zu verschwinden. Genau dafür war das 2>&1 da.

MERKE: Die drei Cron-Klassiker: relativer Pfad, fehlendes x-Bit, leeres PATH. Und ohne >> log 2>&1 am Ende debuggst du ein Gespenst.

Das Übungs-Terminal

Lesen ist nett, tippen bleibt hängen. Hier läuft ein simuliertes Linux mit echtem Dateisystem: Home-Verzeichnis, Logs, Configs. Alle Befehle aus den Modulen funktionieren. Du kannst nichts kaputt machen, und genau dafür ist es da. Tipp help für den Überblick.

ich@linux8x:~bash 5.2
ich@linux8x:~$