1. Docker CLI: Sinn und Zweck
Docker CLI wird gebraucht, um aufzuhören zu raten und anfangen, sich auf Fakten zu stützen. Wenn du jemals versucht hast, das Problem „bei mir läuft es, bei dir nicht" zu beheben, kennst du diesen Schmerz bereits: Ohne klare beobachtbare Schritte wird alles schnell zu Schamanismus. Docker Desktop ist praktisch, lehrt aber kaum, über den Zustand des Systems nachzudenken. Docker CLI hingegen lehrt das: Es gibt Befehle, es gibt Zustände, es gibt Fakten. Auf ihnen baut die Diagnose auf – was heruntergeladen, was gestartet, was tatsächlich läuft.
Den Unterschied zwischen image, container, registry und Compose haben wir bereits besprochen; jetzt muss diese Karte in ein alltägliches CLI-Szenario übersetzt werden. Das grundlegende Arbeitsminimum ist hier: Image holen → Container starten → Gestartetes sehen. Ohne diesen Zyklus wird es weder möglich sein, eine Spring Boot-Anwendung zu containerisieren, noch Fehler zu analysieren oder Kollegen verständlich zu erklären, was man überhaupt macht. Deshalb beginnen wir bewusst mit fertigen Images (ubuntu, nginx), um zu lernen, den Lebenszyklus eines Containers zu steuern, anstatt mit einem Dockerfile zu streiten, das noch nicht existiert.
Um dieses Modell leichter im Kopf zu behalten, hier eine kurze Analogie aus der Java-Welt. Stell dir vor, image ist ein „fertiges Artefakt" (ähnlich wie eine jar-Datei, die bereits gebaut und veröffentlicht wurde), und container ist ein konkreter laufender Prozess java -jar ... mit seiner eigenen PID, Ports und Logs. Docker CLI ist dein jps + ps + kill + tail, nur in Container-Terminologie.
2. docker pull: Image laden
Der Befehl docker pull ist wichtig, weil er das Laden des Images vom Starten eines Containers trennt. Solange du nicht auf die Fragen „Woher habe ich dieses Image?" und „Warum war es gestern eines und heute ein anderes?" stößt, scheint er langweilig. Aber in Docker ist es nützlich, einen einfachen Gedanken zu behalten: Zuerst bekomme ich das Artefakt, und dann starte ich es. Dann wird die Arbeit sinnvoll und sieht nicht aus wie „Taste gedrückt, es macht irgendwas von selbst".
Technisch lädt docker pull das Image aus der registry (standardmäßig meistens Docker Hub) und legt es in den lokalen Speicher der Docker Engine. Wichtig: Nach pull hast du noch keinen Container. Du hast nur eine „Startvorlage" vorbereitet. Das ist wie eine Abhängigkeit aus einem Maven-Repository herunterzuladen: Heruntergeladen, aber die Anwendung startet davon alleine nicht.
Das erste Beispiel – maximal ehrlich und ohne Tücken:
# Image mit explizitem Tag laden, damit das Beispiel reproduzierbar ist
docker pull ubuntu:22.04
Hier ist ubuntu der Name des Image-Repositories, und 22.04 ist der Tag (tag). Man kann den Tag als „Version" verstehen. Ja, später im Kurs werden wir tiefer über Reproduzierbarkeit sprechen, aber am Anfang gilt eine einfache Regel: Wenn du möchtest, dass Beispiele sich heute und morgen gleich verhalten, schreibe einen expliziten Tag, wie im obigen Beispiel, und verlasse dich nicht darauf, „was gerade latest ist".
Noch ein nützlicher Punkt: Bezüglich seines Verhaltens ist docker pull fast idempotent. Wenn das Image bereits lokal vorhanden ist und übereinstimmt, lädt Docker nicht alles neu herunter: Es prüft den Zustand und meldet, dass alles aktuell ist. Das ist gut: Man verschwendet nicht jedes Mal Bandbreite, wenn man den Befehl wiederholt.
3. docker run: Container aus Image starten
Der Befehl docker run ist der Moment, in dem das abstrakte Wort „Container" zu einem echten Objekt wird: Man kann ihn stoppen, löschen, untersuchen und, was erfreulich ist, kontrolliert kaputt machen. Wichtig ist, sofort festzuhalten: docker run macht zwei Dinge auf einmal – erstellt einen neuen Container aus dem Image und startet ihn sofort. Das ist wie „einen neuen Prozess erstellen und sofort starten", wobei jeder Aufruf von run eine neue Instanz erstellt und keine alte wiederverwendet.
Dieser Befehl hat eine sehr nützliche „Form", die man einmal sehen sollte:
# Befehlsvorlage: Optionen → Image → (optional) Befehl innerhalb des Containers
docker run [OPTIONS] IMAGE[:TAG] [COMMAND] [ARG...]
Das bedeutet: Du wählst ein Image und kannst dann optional den Befehl überschreiben, den der Container ausführt. Das ist besonders gut an einem einfachen Beispiel zu sehen, wo der Container genau so lange lebt, wie der Befehl darin lebt:
# Einmaliger Start: Der Container führt den Befehl aus und endet sofort
docker run ubuntu:22.04 echo "hello from container"
Dieser Container wird erstellt, führt echo "hello from container" aus und endet. Das ist kein „Docker kaputt", sondern ein normales Modell: Ein Container lebt, solange der Hauptprozess lebt. Wenn der Hauptprozess endet, endet auch der Container. Später heute werden wir lernen, „normal beendet" von „abgestürzt" zu unterscheiden, aber schon jetzt ist es nützlich zu sehen, dass Einmal-Container ein absolut normales Szenario sind.
Für Java-Entwickler ist das besonders wichtig. Ein Spring Boot-Service ist ein langlebiger Prozess, also ein Server. Das bedeutet, den Container muss man so starten, dass darin ein „langlebiger" Prozess läuft, zum Beispiel java -jar .... Aber jetzt trainieren wir an fertigen Images, um nicht zwei Schwierigkeiten gleichzeitig zu vermischen.
4. --name: Sinnvolle Container-Namen
Die ersten Docker-Starts verlaufen sehr leicht im Modus „ich habe irgendetwas gestartet... wie kann ich es jetzt stoppen?". Docker kann selbst einen Namen für den Container erfinden (oft einen witzigen), aber für das Lernen und für normale Teamarbeit ist es besser, sich sofort eine einfache Disziplin anzugewöhnen: dem Container einen expliziten Namen geben.
Ein Name ist nicht nur Komfort, sondern eine Möglichkeit, Befehle lesbar und wiederholbar zu machen. Wenn du deinen Container demo-nginx nennst, sehen die nächsten Schritte aus wie ein normales menschliches Szenario: „demo-nginx stoppen", „demo-nginx ansehen", „demo-nginx löschen". Ist der Name zufällig, fängst du an, Container-IDs zu kopieren, durcheinanderzukommen, und nach fünfzehn Minuten kommt es dir vor, als würde Docker dich nicht respektieren. (Tut er doch. Er schweigt es nur.)
Beispiel für das Starten eines Containers mit Namen:
# -d: Im Hintergrund starten (Terminal kehrt sofort zurück)
# --name: Container einen verständlichen Namen geben für weitere Befehle
docker run -d --name demo-nginx nginx:alpine
Hier haben wir bereits -d (Hintergrundmodus) hinzugefügt, gehen aber noch nicht tiefer darauf ein: Wir halten nur fest, dass --name ruhig mit anderen Optionen kombiniert werden kann.
Eine praktische Gewohnheit für die Zukunft: Wähle Namen, die die Rolle widerspiegeln. demo-nginx ist besser als server. Und catalog-app (wenn wir zu unserem Lehrservice kommen) ist besser als meincontainer2-final-final.
5. -d: Hintergrundmodus
Das Flag -d (detached mode) wird für eine sehr einfache Sache benötigt: Wenn ein Container ein langlebiger Prozess ist (Webserver, Datenbank, Broker), möchte man nicht, dass er das Terminal blockiert. Man will den Prompt zurückbekommen und weiterarbeiten: Status ansehen, curl ausführen, API prüfen. Dafür gibt es -d.
Hier gibt es einen wichtigen Punkt für Anfänger: Wenn man einen Container mit -d startet, kehrt das Terminal tatsächlich sofort zurück, und Docker gibt eine lange Container-ID aus. Es ist sehr leicht, den falschen Schluss zu ziehen: „Er hat sich bereits beendet". Nein, er ist einfach in den Hintergrund gegangen. Um zu prüfen, ob der Container lebt, verwenden wir docker ps – das ist der nächste Baustein dieser Vorlesung.
Ein einfacher Hintergrundstart:
# Derselbe Start, aber hier fixieren wir bewusst die Idee: Container geht in den Hintergrund
docker run -d --name demo-nginx nginx:alpine
Danach gibt der Befehl so etwas wie eine lange ID zurück (sieht aus wie ein Hash). Diese ID kann man verwenden, aber für Anfänger ist es einfacher, sich auf den Namen zu stützen – deshalb haben wir --name angegeben.
Wichtig: Wir veröffentlichen noch keine Ports und versuchen nicht, nginx im Browser zu öffnen. Das ist ein separates großes Thema (und es kommt später heute). Jetzt müssen wir den Mechanismus verstehen: Der Container ist gestartet, aber wir müssen das mit einem Beobachtungswerkzeug beweisen und nicht auf Glück hoffen.
6. docker ps: Was gerade wirklich läuft
Eine der nützlichsten Gewohnheiten, die Docker lehrt, ist: nicht raten. Man muss den Container nicht „fühlen": Man muss ihn in der Liste der laufenden sehen. Dafür gibt es docker ps – einen Befehl, der nur die aktuell laufenden Container zeigt.
Führe nach dem Start von demo-nginx Folgendes aus:
# Zeigt nur Container, die gerade ausgeführt werden
docker ps
Normalerweise sieht man eine Tabelle mit Spalten wie CONTAINER ID, IMAGE, COMMAND, STATUS, PORTS, NAMES. Auch wenn die Hälfte dieser Wörter gerade wie „Englisch für Erwachsene" aussieht, kann man bereits das Wichtigste herausfiltern: Ist ein Container mit dem Namen demo-nginx in der Liste und welchen Status hat er.
Beachte: In der Ausgabe von docker ps sieht man oft, wie lange der Container läuft, zum Beispiel Up 10 seconds. Das ist ein sehr nützliches schnelles Signal. Wenn du erwartet hast, dass der Container lange läuft, er aber nicht in der Liste ist – das ist kein Grund zur Panik, sondern ein Anlass, zum nächsten Diagnoseschritt überzugehen (der in den nächsten Vorlesungen des Tages kommt).
Noch ein wichtiger Punkt: docker ps zeigt nur laufende Container. Wenn ein Container schnell beendet wurde, sieht man ihn hier nicht. Das ist kein Bug, sondern Teil des Modells. Für das vollständige Bild gibt es docker ps -a, aber das ist die nächste Vorlesung – nicht vorspringen. Jetzt reicht es, die Regel zu akzeptieren: docker ps beantwortet die Frage „Was lebt gerade?".
7. Grundlegender Zyklus pull → run → ps
Mini-Schema pull → run → ps
Manchmal ist es für Anfänger einfacher, sich kein einzelnes Befehle, sondern ein kurzes Szenario zu merken. Für diese Vorlesung ist es so: „Image holen → Container starten → sicherstellen, dass er wirklich gestartet ist". Dieser Zyklus ähnelt einem normalen Ingenieur-Ritual: Erst Artefakt vorbereiten, dann starten, dann Status prüfen.
Hier das Schema als einfaches Flowchart:
flowchart TD
A["docker pull ubuntu:22.04 Image geladen"] --> B["docker run ... Container erstellt und gestartet"]
B --> C["docker ps Bestätigt, dass er lebt"]
C --> D["Weiter im Kurs: stop/rm, logs, inspect, Ports..."]
Der Sinn des Schemas liegt nicht in schönen Pfeilen, sondern in Disziplin. In Docker verschwindet sehr viel Magie, wenn man sich die einfache Frage stellt: „In welchem Schritt bin ich gerade? Habe ich das Image schon heruntergeladen? Habe ich den Container schon gestartet? Habe ich wirklich geprüft, dass er läuft?"
Wenn du dich dabei ertappst zu denken „Ich habe doch wohl gestartet...", kehre zu diesem Schema zurück. Es rettet wirklich Nerven.
Das Modell im Kopf eines Java-Entwicklers
Wenn du anfängst, unseren Container-Ready Catalog Service zu containerisieren (später, nicht heute), werden diese Befehle buchstäblich jeden Tag verwendet. Nur statt nginx:alpine hast du das Image deines Services. Aber das Denkmodell bleibt dasselbe: Image als Artefakt, Container als laufender Prozess, docker ps als Fakt, nicht als Meinung.
Sehr wichtig: Wir lernen Docker nicht als eigene Religion. Wir lernen Docker als Schicht über der gewohnten Entwicklung. Du kannst Spring Boot bereits lokal starten, weißt bereits, dass ein Server ein Prozess ist, der laufen und einen Port lauschen muss. Docker fügt dazu Reproduzierbarkeit und bequemes Umgebungsmanagement hinzu, aber man muss trotzdem mit den Grundlagen beginnen: Lernen, einen Container mit den Händen zu steuern, ohne ihn mit dem Image zu verwechseln und ohne ihn nach dem ersten Start zu verlieren.
Und noch ein Gedanke, der später das Leben sehr erleichtern wird. In Docker muss man ständig die Situationen unterscheiden „ich arbeite mit einer Vorlage" und „ich arbeite mit einer gestarteten Instanz". In der Java-Welt ist das ungefähr wie der Unterschied zwischen einer jar-Datei auf der Festplatte und einem JVM-Prozess, der gerade läuft. Wenn du diesen Unterschied spürst, wird Docker viel ruhiger.
8. Typische Fehler bei pull/run/ps
Fehler Nr. 1: Image und Container verwechseln.
Das ist die häufigste Verwirrung am Anfang. Es scheint, als würde „ich habe ubuntu heruntergeladen" bedeuten „ich habe ein laufendes ubuntu". Aber pull startet nichts. Ein Image ist nur eine Vorlage. Ein Container entsteht erst nach run. Eine gute Selbstprüfung: Stelle dir die Frage „Will ich gerade herunterladen oder starten?" – und der Befehl wird fast immer offensichtlich.
Fehler Nr. 2: Keinen Tag angeben und versehentlich auf latest leben.
Anfänger schreiben oft docker pull ubuntu oder docker run nginx und bekommen „irgendetwas, das läuft". Und dann fängt das Beispiel eine Woche später an, sich anders zu verhalten. Das liegt daran, dass Docker ohne Tag normalerweise latest annimmt, und das kann sich ändern. Zu Kursbeginn reicht eine einfache Disziplin: Schreibe in Lehrbefehlen einen expliziten Tag (ubuntu:22.04, nginx:alpine), um weniger unerwartete Unterschiede zu erleben.
Fehler Nr. 3: Denken, dass docker run einen alten Container „wiederverwendet".
Im Kopf lebt oft die Gewohnheit „noch einmal starten". Aber docker run erstellt einen neuen Container. Wenn man docker run ... zweimal ausführt, hat man zwei verschiedene Container, auch wenn das Image dasselbe ist. Das ist weder gut noch schlecht – es ist einfach das Modell. Später werden wir lernen, zu stoppen und aufzuräumen, aber schon jetzt ist es nützlich, kein „Neustart derselben Instanz" zu erwarten.
Fehler Nr. 4: Mit -d starten und denken, der Container sei beendet, weil das Terminal zurückgekehrt ist.
Das Terminal kehrt zurück, weil der Container im Hintergrund läuft. Die Prüfung ist hier immer dieselbe: docker ps. Wenn der Container in der Liste ist und der Status Up ... lautet, lebt er. Wenn nicht – dann hat er sich beendet, und danach muss man herausfinden warum.
Fehler Nr. 5: Dem Container keinen Namen geben und dann in zufälligen IDs versinken.
Anfangs scheint es, dass kein Name nötig ist: „Ich sehe doch einen Container". Dann kommt ein zweiter, dritter, zehnter – und man fängt an zu verwechseln, welcher davon der eigene ist. --name ist ein billiger Weg, die Welt verständlicher zu machen. Besonders wenn man nicht demo-nginx startet, sondern seinen eigenen Service und eine Abhängigkeit daneben.
GO TO FULL VERSION