Docker endlich verstanden — nach zwei Jahren Copy-Paste
Zwei Jahre lang habe ich Befehle aus Anleitungen kopiert. Dann habe ich mich hingesetzt und die vier Begriffe verstanden, um die es eigentlich geht — und plötzlich ergab alles Sinn.
953 Wörter · 5 Min. Lesezeit
- infrastruktur
- docker
- lernen
Ich habe Docker jahrelang benutzt, ohne es zu verstehen. Ich habe docker compose up -d getippt, weil es in der Anleitung stand, und wenn etwas nicht ging, habe ich Container gelöscht und neu erzeugt, bis es wieder ging.
Im August habe ich mir zwei Abende genommen und die Grundlagen von unten aufgerollt. Danach war es ein Werkzeug statt einer Beschwörungsformel, und dieser Eintrag ist der Versuch, das weiterzugeben, was mir gefehlt hat.
Die vier Begriffe
Ein Abbild ist eine Vorlage. Es ist schreibgeschützt und besteht aus Schichten. Jede Anweisung in der Baudatei erzeugt eine neue Schicht auf der vorherigen.
Ein Container ist eine laufende Instanz davon. Er bekommt eine dünne, beschreibbare Schicht obendrauf. Alles, was dort hineingeschrieben wird, ist weg, wenn der Container gelöscht wird. Genau das ist der Punkt, an dem die meisten anfangs Daten verlieren.
Ein Volume ist Speicher außerhalb des Containers. Es überlebt den Container. Alles, was bleiben soll — Datenbanken, hochgeladene Dateien, Konfiguration —, gehört dorthin.
Ein Netzwerk verbindet Container. Container im selben Netzwerk erreichen sich über ihren Namen. Das ist der Grund, warum in Konfigurationen db statt einer IP-Adresse steht.
Diese vier Begriffe zu trennen war der eigentliche Durchbruch. Praktisch jedes Problem, das ich vorher hatte, war eine Verwechslung von zweien davon.
Die Schichten, und warum die Reihenfolge zählt
Das zweite Aha-Erlebnis war das Verständnis der Schichten. Jede Zeile einer Baudatei erzeugt eine Schicht, und beim erneuten Bauen werden alle Schichten bis zur ersten Änderung wiederverwendet.
Falsch:
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci
CMD ["node", "index.js"]Hier wird bei jeder Codeänderung npm ci erneut ausgeführt, weil COPY . . davor liegt und sich geändert hat. Jeder Bau dauert Minuten.
Richtig:
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "index.js"]Jetzt bleibt die Installationsschicht erhalten, solange sich die Abhängigkeiten nicht ändern. Der Bau dauert Sekunden.
Diese eine Umstellung hat meine Bauzeiten um mehr als das Zehnfache verkürzt, und ich habe zwei Jahre lang gewartet, ohne zu wissen warum.
Tipp
Die Faustregel: Was sich selten ändert, kommt nach oben. Was sich oft ändert, nach unten. Systempakete zuerst, dann Abhängigkeiten, dann der eigene Code. Wer das umdreht, baut bei jeder Kleinigkeit alles neu.
Was mich am meisten überrascht hat
Container sind keine virtuellen Maschinen. Sie teilen sich den Kern des Wirtssystems und isolieren nur Prozesse, Dateisystem und Netzwerk. Deshalb starten sie in Millisekunden. Und deshalb kann ein Container mit zu vielen Rechten sehr wohl auf das Wirtssystem wirken — Isolation ist nicht dasselbe wie eine Sicherheitsgrenze.
Ein Container sollte einen Prozess haben. Nicht, weil mehrere technisch unmöglich wären, sondern weil das gesamte Modell darauf beruht: Wenn der Hauptprozess endet, endet der Container. Wer zwei Dienste hineinpackt, muss selbst dafür sorgen, dass ein Fehler des einen bemerkt wird.
Benutzerrechte gehen nicht automatisch auf. Der Prozess im Container läuft standardmäßig als root, und geschriebene Dateien in einem Volume gehören dann root — auf dem Wirtssystem. Wer die Dateien danach mit einem anderen Benutzer anfassen will, hat ein Rechteproblem. Die Lösung ist, im Abbild einen Benutzer mit passender Nummer anzulegen.
Genau dieser Punkt hat mich später bei einem ganz anderen Aufbau wieder eingeholt, als ich fremde Anwendungen als Container betrieben habe: Die Nummer des Benutzers muss zur Umgebung passen, sonst tut die Anwendung nichts und die Fehlermeldung führt in die Irre.
Was ich seitdem anders mache
Logs kommen auf die Standardausgabe. Kein Schreiben in Dateien im Container. Das ist die einzige Art, wie ein Protokoll außerhalb sichtbar wird.
Konfiguration über Umgebungsvariablen, nicht über Dateien, die ins Abbild gebaut werden. Sonst braucht man für jede Umgebung ein eigenes Abbild.
Ein Abbild wird einmal gebaut und überall benutzt. Wenn ich für die Testumgebung ein anderes baue als für die Produktion, teste ich nicht das, was später läuft.
Feste Versionen statt latest. node:22-alpine statt node:latest. Sonst ändert sich die Grundlage unter einem, ohne dass man etwas getan hat — und die Fehlersuche beginnt bei der falschen Frage.
Die Befehle, die ich täglich benutze
Nach den zwei Abenden hat sich ein kleiner Satz Befehle herausgebildet, mit dem ich praktisch alles erledige.
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
docker logs -f --tail 100 <name>
docker exec -it <name> sh
docker inspect <name> --format '{{json .State}}'
docker stats --no-streamDer erste formatiert die Übersicht so, dass sie in eine Zeile passt. Der zweite ist der, den ich am häufigsten brauche. Der dritte öffnet eine Sitzung im Container — nützlich und mit einer Warnung verbunden: Alles, was man dort ändert, ist beim nächsten Neustart weg, wenn es nicht in einem dauerhaften Bereich liegt.
Der vierte beantwortet die Frage, warum ein Container nicht läuft. Der Zustand enthält den Beendigungscode, und der sagt oft mehr als das Protokoll: Ein bestimmter Wert steht für „von außen beendet", ein anderer für „vom System wegen Speichermangels beendet".
Der Fehler, den ich am häufigsten sehe
Wenn ich anderen bei Container-Problemen helfe, ist es in mehr als der Hälfte der Fälle dasselbe: Daten liegen im Container statt in einem dauerhaften Bereich.
Das Muster ist immer gleich. Etwas funktioniert, wird konfiguriert, läuft wochenlang. Dann kommt ein Update, der Container wird neu erzeugt, und alles ist weg.
Die Prüfung dafür ist einfach und sollte zur Gewohnheit werden: Was von dem, was hier entsteht, muss den nächsten Neustart überleben? Für jede Antwort ein dauerhafter Bereich, ausdrücklich eingerichtet.
Und die Gegenprobe, die ich vor jedem echten Einsatz mache: Container löschen, neu erzeugen, nachsehen, ob alles noch da ist. Zwei Minuten Aufwand, und sie beantworten eine Frage, die sonst irgendwann unter Zeitdruck beantwortet wird.
Warum dieser Eintrag existiert
Nicht, weil daran etwas neu wäre. Sondern weil ich zwei Jahre lang ein zentrales Werkzeug benutzt habe, ohne es zu verstehen — und weil ich glaube, dass das häufiger vorkommt, als man zugibt.
Die zwei Abende, die es gekostet hat, das nachzuholen, haben sich innerhalb einer Woche bezahlt gemacht. Der Grund, warum ich sie so lange nicht investiert habe, war einfach: Es funktionierte ja irgendwie. Genau das ist der Zustand, in dem man am meisten Zeit verliert, ohne es zu bemerken.