Zum Inhalt springen
labkompass

Guides · Docker

Docker Grundlagen: Container, Images, Volumes und Netzwerke

Docker von Grund auf: Image und Container, Lebenszyklus, Volumes, Ports, Netzwerke und Umgebungsvariablen – mit getesteten Befehlen und einem Compose-Beispiel.

Von Niclas Hennrich · Veröffentlicht: · 12 Min. Lesezeit · Einsteiger

Getestet mit

  • Docker Engine 29.9.0
  • Docker Compose 5.6.0
  • Debian 13

Technisch verifiziert am

Reality Check

Stand: Oktober 2026

8 Quellen · Was die Stufen bedeuten


Kurz gesagt: Docker startet Programme als Container: abgeschottete Prozesse, die aus einem unveränderlichen Image entstehen und jederzeit weggeworfen und neu erzeugt werden können. Damit dabei nichts verloren geht, liegen Daten in Volumes; über Port-Freigaben wird ein Dienst von außen erreichbar, über eigene Netzwerke finden sich Container gegenseitig per Namen. Wer diese vier Bausteine versteht, kann fast jede Homelab-Anleitung nachvollziehen.

Dieser Guide richtet sich an alle, die Docker bisher nur aus kopierten Befehlen kennen – oder noch gar nicht. Du arbeitest alles einmal auf der Kommandozeile durch, mit kleinen Wegwerf-Containern, die du am Ende wieder entfernst. Die Befehle und Ausgaben stammen aus einem Test mit Docker Engine 29.9.0 und Docker Compose 5.6.0 unter Debian 13; die Ausgaben sind gekürzt.

Voraussetzungen #

  • Ein Linux-Host (VM, Mini-PC oder Testrechner) mit installiertem Docker Engine und Compose-Plugin. Die Installation über das offizielle apt-Repository zeigt der Guide Portainer installieren Schritt für Schritt – den Portainer-Teil kannst du dort vorerst überspringen.
  • Ein Terminal mit Root-Rechten. Die Befehle unten sind ohne sudo geschrieben; arbeitest du als normaler Benutzer, stellst du sudo voran.
  • Etwa 30 Minuten und kein Produktivsystem: Die Beispiele legen Container, ein Volume und ein Netzwerk an und räumen sie wieder ab.

Lernziel: Du kannst einen Container starten, prüfen und entfernen, Daten dauerhaft in einem Volume ablegen, einen Port gezielt freigeben, zwei Container über ein eigenes Netzwerk verbinden und dasselbe Setup als compose.yaml beschreiben.

Die Bausteine: Image, Container, Registry und Tag #

Image #

Ein Image ist eine fertige Vorlage: ein Paket aus Dateisystem, Bibliotheken und einem Startbefehl für ein Programm. Es wird in Schichten (Layers) gebaut, und jede Schicht hält eine Änderung am Dateisystem fest. Wichtig für den Alltag: Ein Image wird nach dem Bauen nicht mehr verändert. Eine neue Version ist immer ein neues Image.

Container #

Ein Container ist ein laufender (oder gestoppter) Prozess, der aus einem Image gestartet wurde. Er bekommt eine eigene Sicht auf Dateisystem, Prozesse und Netzwerk, teilt sich aber den Kernel des Hosts – das unterscheidet ihn von einer virtuellen Maschine, die einen eigenen Kernel mitbringt. Aus einem Image kannst du beliebig viele Container starten.

Was ein Container während der Laufzeit in sein eigenes Dateisystem schreibt, landet in einer dünnen, beschreibbaren Schicht über dem Image. Diese Schicht verschwindet, wenn du den Container löschst. Daraus folgt die wichtigste Regel im Umgang mit Docker: Container sind Wegwerfware, Daten gehören in Volumes.

Registry und Tag #

Eine Registry ist ein Server, von dem Docker Images herunterlädt – Standard ist Docker Hub. Der Tag hinter dem Doppelpunkt benennt eine Variante des Images: nginx:1.30-alpine ist der Webserver nginx in der Versionslinie 1.30 (der aktuellen Stable-Linie) auf Basis des schlanken Alpine Linux. Fehlt der Tag, nimmt Docker latest. Das klingt nach „neueste Version“, ist aber nur ein Name, den der Herausgeber auf irgendeine Version zeigen lässt. Für Dienste, die dauerhaft laufen, ist ein fester Tag die bessere Wahl.

Der erste Container in fünf Schritten #

1. Prüfen, ob Docker läuft. Der erste Befehl fragt den Daemon (den Hintergrunddienst, der die Container tatsächlich verwaltet) nach seiner Version, der zweite zeigt die Version des Compose-Plugins:

docker version --format '{{.Server.Version}}'
docker compose version
29.9.0
Docker Compose version v5.6.0

Kommt hier eine Fehlermeldung mit permission denied, fehlen dir die Rechte am Docker-Socket – siehe Typische Probleme.

2. Image herunterladen. docker pull holt das Image aus der Registry, docker image ls zeigt, was lokal vorhanden ist:

docker pull nginx:1.30-alpine
docker image ls nginx

3. Container starten. Der folgende Befehl startet nginx im Hintergrund (-d), gibt ihm den Namen web und macht Port 80 des Containers auf Port 8080 des Hosts erreichbar:

docker run -d --name web -p 8080:80 nginx:1.30-alpine

Docker antwortet mit der langen ID des neuen Containers.

4. Prüfen, ob er läuft. docker ps listet laufende Container, curl fragt die Startseite ab:

docker ps
curl -s http://localhost:8080 | head -4
CONTAINER ID   IMAGE               STATUS         PORTS                                     NAMES
a1b2c3d4e5f6   nginx:1.30-alpine   Up 5 seconds   0.0.0.0:8080->80/tcp, [::]:8080->80/tcp   web
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

Die Spalte PORTS zeigt schon etwas Wichtiges: Der Port ist auf allen Adressen des Hosts freigegeben, IPv4 (0.0.0.0) wie IPv6 ([::]). Dazu gleich mehr.

5. Logs ansehen. Alles, was das Programm im Container auf die Standardausgabe schreibt, liest du mit docker logs:

docker logs --tail 3 web

Darin taucht auch deine curl-Anfrage von eben als Zugriffszeile auf.

Der Lebenszyklus eines Containers #

Ein Container durchläuft immer dieselben Zustände: angelegt, laufend, beendet, gelöscht. Die passenden Befehle:

docker stop web      # Prozess sauber beenden, Container bleibt erhalten
docker ps -a         # zeigt auch beendete Container
docker start web     # denselben Container wieder starten
docker restart web   # stoppen und starten in einem Schritt
docker rm -f web     # Container stoppen und löschen

Nach docker stop steht der Container in docker ps -a mit dem Status Exited (0) – die Zahl in Klammern ist der Exit-Code des Programms, 0 bedeutet „ordentlich beendet“. Ein laufender Container lässt sich nicht mit einem einfachen docker rm löschen; Docker verweigert das mit dem Hinweis container is running: stop the container before removing or force remove. Das -f erzwingt Stoppen und Löschen in einem Schritt. Weil damit die beschreibbare Schicht des Containers verloren geht, nutze es nur für Container, deren Daten in Volumes liegen oder die du ohnehin wegwerfen willst.

Wenn ein Container sofort wieder aus ist #

Ein Container läuft genau so lange wie sein Hauptprozess. Beendet sich der Prozess wegen eines Fehlers, ist auch der Container beendet. Das lässt sich gefahrlos nachstellen:

docker run --name kaputt alpine:3.22 sh -c 'echo "Konfiguration fehlt" >&2; exit 3'
docker ps -a --filter name=kaputt
docker logs kaputt
docker inspect -f '{{.State.ExitCode}}' kaputt
docker rm kaputt

Der Status lautet Exited (3), docker logs zeigt die Fehlermeldung des Programms, und docker inspect liefert den Exit-Code 3. Diese Reihenfolge – Status, Logs, Exit-Code – ist bei jedem Container, der nicht startet, der erste Diagnoseschritt.

Neustart-Richtlinien #

Ob Docker einen Container nach einem Absturz oder einem Neustart des Hosts wieder hochfährt, legt die Restart-Policy fest:

PolicyVerhalten
noStandard: kein automatischer Neustart
on-failureNeustart nur, wenn das Programm mit einem Fehlercode endet
alwaysNeustart in jedem Fall; auch ein von Hand gestoppter Container läuft nach einem Neustart des Docker-Dienstes wieder
unless-stoppedwie always, aber ein von dir gestoppter Container bleibt nach einem Neustart des Docker-Dienstes aus

Für typische Homelab-Dienste passt unless-stopped: Der Dienst überlebt Abstürze und Reboots, respektiert aber ein bewusstes docker stop.

docker run -d --name web --restart unless-stopped -p 8080:80 nginx:1.30-alpine
docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' web

Die Ausgabe ist unless-stopped. Räum den Testcontainer danach wieder ab, sonst ist Port 8080 im Compose-Beispiel unten belegt:

docker rm -f web

Eine Restart-Policy repariert aber keinen Fehler: Scheitert das Programm bei jedem Start, siehst du in docker ps -a den Status Restarting (1), und der Zähler RestartCount in docker inspect steigt immer weiter. Dann helfen nur die Logs.

Daten behalten: Volumes und Bind Mounts #

Es gibt zwei übliche Wege, Daten außerhalb des Containers abzulegen:

  • Ein Volume ist ein von Docker verwalteter Speicherbereich. Du sprichst es über seinen Namen an, Docker legt die Daten auf Linux unter /var/lib/docker/volumes/ ab.
  • Ein Bind Mount blendet ein bestimmtes Verzeichnis des Hosts in den Container ein, etwa /opt/app/config. Praktisch, wenn du die Dateien auch vom Host aus bearbeiten willst.

Dass ein Volume den Container überlebt, zeigt ein kurzer Versuch. Der Schalter --rm sorgt dafür, dass Docker den Container nach dem Ende automatisch löscht:

docker volume create web-daten
docker run --rm -v web-daten:/daten alpine:3.22 sh -c 'echo hallo > /daten/test.txt'
docker run --rm -v web-daten:/daten alpine:3.22 cat /daten/test.txt
docker run --rm alpine:3.22 cat /daten/test.txt
hallo
cat: can't open '/daten/test.txt': No such file or directory

Der erste Container schreibt die Datei und ist danach weg. Ein zweiter, völlig neuer Container mit demselben Volume liest sie problemlos. Der dritte Container hat kein Volume eingebunden – für ihn existiert die Datei nicht. Wo das Volume auf dem Host liegt, verrät docker volume inspect web-daten --format '{{.Mountpoint}}'. Das Test-Volume löschst du mit docker volume rm web-daten – der Befehl entfernt die Daten darin endgültig, bei echten Volumes also nur mit Backup.

Bei Bind Mounts kommt eine Stolperfalle dazu: Die Dateirechte des Host-Verzeichnisses gelten auch im Container. Läuft das Programm im Container als Benutzer mit der ID 1000 und gehört das Verzeichnis root, scheitert jeder Schreibversuch mit Permission denied. Die Lösung ist, den Besitzer des Verzeichnisses passend zu setzen (siehe Typische Probleme).

Ports: wie ein Dienst erreichbar wird #

Ein Container hat seine eigene Netzwerkschnittstelle. Von außerhalb des Hosts ist er erst erreichbar, wenn du einen Port veröffentlichst. Die Angabe -p 8080:80 liest sich immer als HOST:CONTAINER: Anfragen an Port 8080 des Hosts landen auf Port 80 im Container. So können zwei Container intern beide Port 80 nutzen und trotzdem nebeneinander laufen – sie bekommen einfach unterschiedliche Host-Ports.

Ohne weitere Angabe gibt Docker den Port auf allen Adressen des Hosts frei. Soll ein Dienst nur lokal oder nur in einem bestimmten Netz erreichbar sein, setzt du die Adresse davor:

docker run -d --name intern -p 127.0.0.1:8081:80 nginx:1.30-alpine
docker run -d --name lan -p 192.168.10.20:8082:80 nginx:1.30-alpine

Der erste Container ist nur vom Host selbst aus erreichbar, der zweite lauscht nur auf der LAN-Adresse 192.168.10.20 (ein Beispielwert – setze die Adresse deines Hosts ein). Die Adresse legt fest, auf welcher Schnittstelle der Port offen ist, nicht wer darauf zugreifen darf: Laut Docker-Doku erreichen auch Geräte aus anderen Netzen den Port, wenn ihr Verkehr zu dieser Adresse geroutet wird. Eine Firewall ersetzt die Bindung also nicht. Die beiden Testcontainer entfernst du mit docker rm -f intern lan wieder.

Netzwerke: wie Container sich finden #

Container, die zusammenarbeiten – etwa eine Anwendung und ihre Datenbank –, müssen sich gegenseitig erreichen. Dafür gibt es Docker-Netzwerke. Der entscheidende Unterschied:

  • Im vorgegebenen Netzwerk bridge, in dem jeder Container ohne weitere Angabe landet, erreichen sich Container nur über ihre wechselnden IP-Adressen.
  • In einem selbst angelegten Netzwerk betreibt Docker einen eigenen DNS-Dienst. Container finden sich dort über ihren Namen.

So siehst du den Unterschied direkt:

docker network create labnetz
docker run -d --name app --network labnetz traefik/whoami:v1.11
docker run --rm --network labnetz alpine:3.22 wget -qO- http://app:80
docker run --rm alpine:3.22 wget -qO- -T 3 http://app:80
Hostname: 9f8e7d6c5b4a
IP: 127.0.0.1
...
wget: bad address 'app:80'

traefik/whoami ist ein winziger Testdienst, der auf jede Anfrage mit Informationen über sich selbst antwortet. Der Container im Netzwerk labnetz erreicht ihn über den Namen app; der Container im Standardnetz kann den Namen nicht auflösen. Merke dir dieses Fehlerbild – bad address heißt fast immer: Die beiden Container hängen nicht im selben selbst angelegten Netzwerk. Beachte auch: app hat keinen veröffentlichten Port und ist trotzdem für Container im selben Netzwerk erreichbar. Eine Datenbank, die nur deine Anwendung braucht, sollte genau so laufen – ohne -p.

Aufräumen:

docker rm -f app
docker network rm labnetz

Umgebungsvariablen: Einstellungen ohne neues Image #

Viele Images lesen ihre Konfiguration aus Umgebungsvariablen – benannten Werten, die beim Start an den Prozess übergeben werden. Mit -e setzt du sie:

docker run --rm -e GRUSS=moin alpine:3.22 sh -c 'echo $GRUSS'

Die Ausgabe ist moin. Welche Variablen ein Image versteht, steht in seiner Dokumentation, meist auf der Docker-Hub-Seite. Für Passwörter und Schlüssel sind Umgebungsvariablen nur die zweitbeste Lösung: Jeder, der den Container per docker inspect ansehen darf, sieht sie im Klartext. Die Docker-Dokumentation empfiehlt für sensible Werte Secrets; im Homelab ist mindestens eine separate .env-Datei mit eingeschränkten Rechten sinnvoll, die nicht in ein Git-Repository wandert. Sie hält Passwörter aus der compose.yaml heraus; per docker inspect bleiben sie im laufenden Container aber sichtbar.

Von docker run zu Docker Compose #

Lange docker run-Zeilen sind schwer zu pflegen. Docker Compose beschreibt dasselbe in einer YAML-Datei namens compose.yaml: welche Dienste (Services) es gibt, welches Image sie nutzen, welche Ports, Volumes und Variablen sie bekommen. Das folgende Beispiel kombiniert alles aus diesem Guide – einen Webserver mit eingebundenem Verzeichnis und den whoami-Testdienst mit einer Umgebungsvariable.

Verzeichnis und Startseite anlegen:

mkdir -p ~/docker-demo/html
cd ~/docker-demo
echo '<h1>Hallo aus dem Bind Mount</h1>' > html/index.html
nano compose.yaml
services:
  web:
    image: nginx:1.30-alpine
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      - ./html:/usr/share/nginx/html:ro
  app:
    image: traefik/whoami:v1.11
    restart: unless-stopped
    environment:
      WHOAMI_NAME: "labkompass"

Der Zusatz :ro bindet das Verzeichnis nur lesend ein – nginx soll die Dateien ausliefern, aber nicht verändern. Starten und prüfen:

docker compose up -d
docker compose ps
curl -s http://localhost:8080
docker compose exec web wget -qO- http://app:80 | grep Name
NAME               IMAGE                  SERVICE   STATUS         PORTS
docker-demo-app-1  traefik/whoami:v1.11   app       Up 2 seconds   80/tcp
docker-demo-web-1  nginx:1.30-alpine      web       Up 2 seconds   0.0.0.0:8080->80/tcp, [::]:8080->80/tcp
<h1>Hallo aus dem Bind Mount</h1>
Name: labkompass

Drei Dinge erledigt Compose dabei von selbst: Es benennt die Container nach dem Verzeichnis (dem Projektnamen), legt für das Projekt ein eigenes Netzwerk docker-demo_default an, und die Dienste finden sich darin über ihren Service-Namen – web erreicht app ohne veröffentlichten Port. Die Variable WHOAMI_NAME ist im Container angekommen, wie die letzte Zeile zeigt.

Logs aller Dienste siehst du mit docker compose logs, gestoppt und entfernt wird das ganze Projekt mit:

docker compose down

down löscht Container und Projektnetzwerk, lässt aber das Verzeichnis html und benannte Volumes in Ruhe. Erst docker compose down -v würde auch die benannten Volumes des Projekts löschen – mit allen Daten darin.

Typische Probleme #

  • Bind for 0.0.0.0:8080 failed: port is already allocated – Ursache: Ein anderer Container oder Dienst nutzt den Host-Port. Lösung: Mit ss -tlnp | grep 8080 nachsehen, wer ihn belegt (bei einem Container steht dort docker-proxy), und einen anderen Host-Port wählen.
  • pull access denied … repository does not exist – Ursache: Tippfehler im Image-Namen. failed to resolve reference … not found – Ursache: Der Tag existiert nicht. Lösung in beiden Fällen: Namen und Tag auf Docker Hub prüfen.
  • permission denied while trying to connect to the docker API – Ursache: Dein Benutzer darf den Docker-Socket nicht ansprechen. Lösung: sudo voranstellen oder den Benutzer bewusst in die Gruppe docker aufnehmen (Sicherheitshinweis unten beachten).
  • Container steht auf Exited (…) – Ursache: Der Hauptprozess hat sich beendet. Lösung: docker logs <name> lesen; die Fehlermeldung des Programms steht fast immer dort.
  • Status Restarting – Ursache: Das Programm scheitert bei jedem Start, die Restart-Policy startet es immer wieder. Lösung: Logs lesen, Fehler beheben, Container neu erzeugen.
  • bad address beim Aufruf eines anderen Containers – Ursache: Die Container hängen nicht im selben selbst angelegten Netzwerk. Lösung: gemeinsames Netzwerk anlegen oder beide Dienste in dieselbe compose.yaml schreiben.
  • Permission denied beim Schreiben in einen Bind Mount – Ursache: Das Host-Verzeichnis gehört einem anderen Benutzer als dem, unter dem das Programm im Container läuft. Lösung: Besitzer anpassen, z. B. chown 1000:1000 /opt/app/data, passend zur Benutzer-ID aus der Doku des Images.
  • did not find expected key aus docker compose config – Ursache: Einrückungsfehler in der YAML-Datei. Lösung: nur Leerzeichen verwenden, konsequent zwei pro Ebene, und die Datei vor dem Start mit docker compose config prüfen lassen.

Sicherheit und Betrieb #

  • Docker-Rechte sind Root-Rechte. Wer Docker steuern darf, kann einen Container mit dem kompletten Host-Dateisystem starten. Die Docker-Dokumentation warnt ausdrücklich, dass die Gruppe docker Rechte auf Root-Niveau verleiht. Nimm nur Benutzer auf, denen du auch Root anvertrauen würdest.
  • Der Docker-Socket ist der Generalschlüssel. Über /var/run/docker.sock wird der Docker-Dienst gesteuert. Ein Container, dem du den Socket einbindest, hat damit dieselbe Macht über den Host. Das betrifft vor allem Verwaltungswerkzeuge wie Portainer – Details und Gegenmaßnahmen im Guide Docker und Portainer absichern.
  • Veröffentlichte Ports umgehen ufw und firewalld. Die Installationsanleitung von Docker weist darauf hin: Ports, die Docker veröffentlicht, laufen an den Regeln dieser Firewalls vorbei. Gib Ports deshalb gezielt auf 127.0.0.1 oder einer LAN-Adresse frei und veröffentliche nur, was wirklich von außen erreichbar sein muss. Die Bindung begrenzt die Adresse, nicht die Absender – den Zugriff selbst regelt weiterhin eine Firewall, etwa am Router.
  • Feste Versionen statt latest. Dann ist ein Update eine bewusste Änderung an der compose.yaml und kein Zufall beim nächsten Pull.
  • Backups betreffen Volumes, nicht Container. Container lassen sich jederzeit aus Image und compose.yaml neu erzeugen. Sichern musst du die Compose-Dateien und die Volumes beziehungsweise Bind-Mount-Verzeichnisse. Befehle wie docker volume rm oder docker volume prune löschen Daten endgültig – probiere sie nur in einer Testumgebung aus und erst, wenn ein Backup existiert. Wie eine solche Sicherung zuhause aussehen kann, beschreibt die Backup-Strategie für zuhause.

Weiterlesen #

Quellen

  1. Offizielle Dokumentation

    Docker Docs — What is a container? (externer Link, neues Fenster)

  2. Offizielle Dokumentation

    Docker Docs — Volumes (externer Link, neues Fenster)

  3. Offizielle Dokumentation

    Docker Docs — Networking overview (externer Link, neues Fenster)

  4. Offizielle Dokumentation

    Docker Docs — Port publishing and mapping (externer Link, neues Fenster)

  5. Offizielle Dokumentation

    Docker Docs — Start containers automatically (externer Link, neues Fenster)

  6. Offizielle Dokumentation

    Docker Docs — Set environment variables within your container's environment (externer Link, neues Fenster)

  7. Offizielle Dokumentation

    Docker Docs — Install Docker Engine on Debian (externer Link, neues Fenster)

  8. Offizielle Dokumentation

    Docker Docs — Linux post-installation steps for Docker Engine (externer Link, neues Fenster)

Häufige Fragen

Ist ein Docker-Container dasselbe wie eine virtuelle Maschine?
Nein. Eine VM bringt ein komplettes Betriebssystem mit eigenem Kernel mit, ein Container ist dagegen ein abgeschotteter Prozess, der den Kernel des Hosts mitbenutzt. Dadurch starten Container in Sekunden und brauchen wenig Overhead, die Trennung zum Host ist aber weniger hart als bei einer VM. Im Homelab kombiniert man oft beides: Docker läuft in einer VM.
Verliere ich meine Daten, wenn ich einen Container lösche?
Alles, was der Container nur in sein eigenes Dateisystem geschrieben hat, ist mit docker rm weg. Daten in einem Volume oder Bind Mount bleiben dagegen erhalten und stehen dem nächsten Container wieder zur Verfügung. Deshalb gehört jedes Verzeichnis, das ein Update überleben soll, in ein Volume – und genau diese Volumes gehören ins Backup.
Brauche ich Docker Compose oder reicht docker run?
Für einen schnellen Test reicht docker run. Sobald ein Dienst dauerhaft laufen soll oder aus mehreren Containern besteht, ist Compose die bessere Wahl: Die compose.yaml hält Image-Version, Ports, Volumes und Variablen in einer Datei fest, die du versionieren und auf einem anderen Host identisch wieder starten kannst.
Warum ist latest als Image-Tag keine gute Idee?
latest ist nur ein Name, keine Versionsangabe. Er kann morgen auf eine ganz andere Version zeigen als heute, und ein docker compose pull holt dann unbemerkt eine neue Hauptversion. Mit einem Versions-Tag wie nginx:1.30-alpine bleibst du auf einer Versionslinie und entscheidest selbst, wann du auf die nächste wechselst.

Teil des Themen-Hubs Docker & Compose im Homelab · Abschnitt „Grundlagen“

Weiterlesen