Zum Inhalt springen
Alle Artikel
Lesen. Lernen. Anwenden.

Das ultimative Docker Cheat Sheet

3. November 202318 Min. LesezeitDeutsch

Dieser Artikel wurde mit ChatGPT automatisch aus dem Englischen ins Deutsche übersetzt.

TLDR; Hol dir das Docker Cheat Sheet als PDF oder Bild. Damit du mitarbeiten kannst, sollte auf deinem Entwicklungsrechner Docker installiert sein. In diesem Artikel schreiben wir eigene Dockerfiles, erstellen Images und führen sie schließlich als Container aus. Den vollständigen Quellcode findest du auf GitHub.

Das ultimative Docker Cheat Sheet herunterladen

Lade das Docker Cheat Sheet herunter, um den Artikel Schritt für Schritt durchzuarbeiten. Teile es gerne mit deinen Kolleginnen, Kollegen und Freunden.

Möchtest du weitere Ressourcen wie diese?

Komm in unsere Community und melde dich für unseren Newsletter an, um bei DevOps auf dem Laufenden zu bleiben. Du erhältst unsere neuesten Ressourcen und Erkenntnisse direkt in dein Postfach!

Was unterscheidet Dockerfile, Image und Container?

Diese Bausteine bauen aufeinander auf. Du brauchst ein Dockerfile, um ein Image zu erstellen, und ein Image, um einen Container zu erstellen.

  • Dockerfile: Der erste Schritt ist das Schreiben eines Dockerfiles. Es ist der Bauplan für ein Docker-Image: eine Textdatei, üblicherweise namens „Dockerfile“ ohne Dateiendung, mit einer Reihe von Anweisungen. Daraus entstehen Schichten, die zwischengespeichert werden können. Wenn du ein Image erneut baust, verwendet Docker diesen Cache. Änderst du eine Anweisung, werden diese und die nachfolgenden Anweisungen erneut ausgeführt.
  • Image: Beim Bauen aus einem Dockerfile entsteht ein Docker-Image. Du kannst es dir wie eine ausführbare Datei vorstellen: So wie du eine Anwendung über ein Symbol auf deinem Desktop startest, kannst du aus einem Image einen Container starten. Das Image enthält den Anwendungscode und seine Abhängigkeiten, einschließlich Laufzeitumgebung und Systembibliotheken. Diese in sich geschlossene Einheit sorgt für Konsistenz und Portabilität zwischen Umgebungen, beispielsweise deinem Entwicklungsrechner und dem Produktionsserver.
  • Container: Ein Container ist eine laufende Instanz eines Docker-Images. Beim Start wird der im Dockerfile festgelegte Befehl ausgeführt. Aus einem Image können viele Container entstehen. Unter Linux läuft der Container als Prozess auf dem Host. Unter Windows oder macOS läuft Docker für Linux-Container in einer virtuellen Maschine. Der Container nutzt den Kernel des Linux-Hosts beziehungsweise dieser VM. Er selbst ist keine virtuelle Maschine. Er hat eine isolierte Sicht auf Prozesse und ein eigenes Dateisystem, was wie eine VM wirken kann. Tatsächlich teilt er sich aber den Kernel mit dem Host oder der VM.

Das Dockerfile bildet also die Grundlage für das Image, aus dem ein Container erstellt wird. Der Container läuft als Prozess auf dem Host, hat dabei jedoch sein eigenes Dateisystem und ist von anderen Prozessen isoliert.

Wie erstellst du ein Dockerfile?

Erstelle dafür eine einfache Textdatei. In diesem Artikel verwenden wir die Kommandozeile:

bash
# create a new file in your current working directory called Dockerfile
$ touch Dockerfile
# open the file in your favorite editor (we are using Visual Studio Code)
# if you do not have the code command installed, you will need to open it manually
$ code Dockerfile

Ein Dockerfile enthält die Anweisungen zum Bauen, Starten und Ausführen deiner Anwendung. Befehle, die du sonst manuell ausführen müsstest, stehen damit in einer Datei. Es beginnt mit einem Basis-Image, oft einer kleinen Linux-Distribution wie Alpine. Für eine eigenständige Binärdatei kann FROM scratch geeignet sein. In diesem Artikel verwenden wir einen Fastify-Server und deshalb ein für Node.js vorbereitetes Alpine-Image. Den vollständigen Code findest du auf GitHub. Mehr zur Auswahl erfährst du in „Wie wählst du ein Docker-Basis-Image aus?“.

Bearbeite nun das Dockerfile in Visual Studio Code:

dockerfile
# the next line sets the base image for this image
# the base image is also based on a Dockerfile
# see: https://hub.docker.com/layers/library/node/18-alpine/images/sha256-a0b787b0d53feacfa6d606fb555e0dbfebab30573277f1fe25148b05b66fa097
# node provides official images for Node.js and
# alpine: a lightweight Linux distribution to reduce image size
FROM node:18-alpine

# sets the working directory inside the image
# all commands after this instruction will be
# executed inside this directory
WORKDIR /app

# copies the package.json and package-lock.json
# from the client (e.g., your server or your development machine)
# into the /app directory inside the image
# before running npm ci to
# get the advantage of layer caching
COPY ./package* .

# installs all node.js dependencies
# npm ci is similar to npm install but intended to be
# used in continuous integration (CI) environments
# it will do a clean installion based on the package-lock.json
RUN npm ci

# copies the source code into the image
COPY . .

# this runs the build command specified in the package.json
RUN npm run build:server

# the EXPOSE instruction does not actually expose the port 300 of this image
# this is documentation so that we know which port we need to expose
# we do this when starting the container with the --publish flag
EXPOSE 3000

# executes the server.js file that is located in the build directory
CMD ["node", "./build/index.js"]

Was ist ein mehrstufiges Dockerfile?

Ein Dockerfile kann mehrere Stufen enthalten. Jede Stufe kannst du dir wie ein eigenes Image vorstellen. Dateien aus einer Stufe lassen sich in einer anderen verwenden. Eine neue Stufe beginnt immer mit FROM <base-image>. Hinweise zur Auswahl findest du in „Wie wählst du ein Docker-Basis-Image aus?“. Mit dem Schlüsselwort as gibst du der Stufe einen Namen. Unsere Client-Anwendung besteht aus einer einfachen HTML- und JavaScript-Datei. Als Webserver verwenden wir NGINX. Der vollständige Code liegt auf GitHub.

Ein typischer Ansatz ist eine builder stage mit einem größeren Basis-Image. Sie enthält alle Programme und Bibliotheken, die zum Bauen des Quellcodes nötig sind.

Die zweite Stufe, die serve stage, verwendet ein kleines Basis-Image. Sie enthält nur die Abhängigkeiten und Bibliotheken, die zum Ausführen und Bereitstellen der Anwendung nötig sind.

So wird das fertige Image kleiner und seine Angriffsfläche geringer. Weniger Systembibliotheken bedeuten weniger potenzielle Angriffspunkte. Mehrstufige Dockerfiles sind nicht auf diesen Anwendungsfall beschränkt und können auch mehr als zwei Stufen enthalten.

bash
# lets create a new Dockerfile for our frontend application
# as we can specify a file in the build command, we name this Dockerfile.client
$ touch Dockerfile.client
# open the file in your favorite editor (we are using Visual Studio Code)
# if you do not have the code command installed, you will need to open it
$ code Dockerfile.client

Hier ist ein Beispiel für eine Anwendung mit mehreren Build-Stufen:

dockerfile
# the base image
# name it builder
# you can reference this stage
# in other stages by this name
FROM node:18-alpine as builder

# working directory inside the image
WORKDIR /app

# copies files from the client to the image
COPY ./package* .

# run a command inside the image
RUN npm ci

# copies files from the client to the image
COPY . .

# run a command inside the container
# this will create a new folder in called dist in our app directory
# inside the dist directory, you will find the
# final HTML and JavaScript file
RUN npm run build:client

# serve stage
# slim nginx base image named as serve
# will start nginx as non root user
FROM nginxinc/nginx-unprivileged:1.24 as serve

# we can now copy things from the first stage to the second
# we copy the build output to the directory where nginx serves files
COPY --from=builder /app/dist /var/www

# we overwrite the default config with our own
# if you take a look at the GitHub repository, you
# see the .nginx directory with the nginx.conf
# here we only use the port 80
# in production, you would also want to make sure
# all requests, even in your internal network or Kubernetes cluster
# is served via HTTPS when dealing with sensible data
COPY --from=builder /app/.nginx/nginx.conf /etc/nginx/conf.d/default.conf

# the EXPOSE instruction does not actually expose the port 80 of this image
# this is documentation so that we know which port we need to expose
# we do this when starting the container with the --publish flag
EXPOSE 80

# The command used when the image is started as a container
# Note: for Docker containers (or for debugging),
# the "daemon off;" directive which is used in this example
# tells nginx to stay in the foreground.
# for containers, this is useful.
# best practice: one container = one process.
# one server (container) has only one service.
CMD ["nginx", "-g", "daemon off;"]

Wie erstellst du ein Docker-Image?

Mit der Docker-CLI bauen wir Images aus unseren Dockerfiles.

bash
# list the directory to make sure you are in the directory with the Dockerfile
$ ls
# if not, change the directory with "cd ./path/to/directory-with-Dockerfile"
# build an Image out of the Dockerfile in the current working directory
$ docker build .
# if you want to build another Dockerfile in this directory, use the --file flag
# e.g., --file <filename>
$ docker build --file Dockerfile.client .

Dafür genügt der Befehl docker build. Ohne Namen und Tag kannst du das Image anschließend nur über seine Image-ID referenzieren.

Wie listest du alle Images auf?

bash
# list all local images on the client (your server or your development machine)
$ docker image ls
# find your image ID

Um beim Bauen einen Namen zu vergeben, verwende die Option --tag, kurz -t. Das brauchst du auch für die Arbeit mit einer Registry wie Docker Hub.

Wie benennst du deine Images?

Verwende das Muster <name>:<tag>. Üblich ist <username>/<repository>:<version>, wobei der Benutzername dem Konto in der Registry entspricht.

bash
# build and tag your image
# a tag consists of a name and a tag, which is separated by a colon (:)
$ docker build --tag examplename/examplerepository-server:0.1.0 .
# or
$ docker build --file Dockerfile.client --tag examplename/examplerepository-client:0.1.0 .
# list all local images
$ docker image ls
# you will see your image with a proper repository name and a tag

Wie erstellst du einen Container?

Ein Container ist eine laufende Instanz eines Images. Du startest ihn mit docker run <image-name>:

bash
# start our image
$ docker run examplename/examplerepository-server:0.1.0
# or
$ docker run examplename/examplerepository-client:0.1.0

Docker speichert Images lokal. docker run sucht zuerst dort nach dem Image. Unser Image ist vorhanden, weil wir es auf demselben Rechner gebaut haben. Fehlt es lokal, sucht Docker auf Docker Hub. Du kannst auch Images aus anderen Registries verwenden, etwa einer selbst betriebenen Registry. Dafür gibst du deren Adresse im Image-Namen an.

bash
# try to find an image on another registry
$ docker run registrydomain.com/examplename/examplerepository-server:0.1.0

Standardmäßig startet der Container im Vordergrund, und dein Terminal bleibt mit ihm verbunden. Wenn du das Terminal beendest, kann dadurch auch der Container gestoppt werden. Soll er unabhängig davon auf deinem Rechner oder Server weiterlaufen, starte ihn im Hintergrund.

Wie lässt du Container im Hintergrund laufen?

Die Option --detach, kurz -d, startet Container im Hintergrund:

bash
# run container in the background
$ docker run --detach examplename/examplerepository-server:0.1.0
# or
$ docker run --detach examplename/examplerepository-client:0.1.0

Der Befehl kehrt zurück und du kannst das Terminal wieder verwenden.

Wie listest du alle Container auf?

Standardmäßig zeigt Docker nur laufende Container an.

bash
# list all running containers
$ docker container ls
# short
$ docker ps

Um auch gestoppte Container anzuzeigen, ergänze --all, kurz -a.

bash
# list all stopped and running containers
$ docker container ls --all

Wie stoppst und entfernst du Container?

Ein gestoppter Container bleibt auf dem System und kann erneut gestartet werden. Möchtest du ihn vollständig entfernen, musst du ihn löschen. Stoppe ihn dafür zuerst.

bash
# stop a container
$ docker container stop <container-id>
# start a container
$ docker container start <container-id>
# restart container
$ docker container restart <container-id>
# remove a stopped container
$ docker container rm <container-id>

Mit --rm beim Start wird ein Container automatisch entfernt, sobald er stoppt. Verwende das nur für zustandslose Container oder sichere ihren Zustand über Volumes. Sonst gehen die im Container gespeicherten Daten beim Entfernen verloren.

bash
# automatically remove a container after it stops
$ docker run --rm examplename/examplerepository-server:0.1.0
# or
$ docker run --rm examplename/examplerepository-client:0.1.0

Was tun, wenn der Container nach dem Schließen des Terminals weiterläuft?

Manchmal werden Signale nicht korrekt an den Container weitergeleitet. Vielleicht hast du das Terminal geschlossen, weil sich der Container mit STRG+C nicht stoppen ließ. Beim erneuten Start meldet Docker dann, dass der Port bereits belegt ist. Das kann bedeuten, dass der alte Container noch läuft. So beendest du ihn sofort:

bash
# kill a container
$ docker kill <container-id>

Wie greifst du vom Host auf Container zu?

Eine Anwendung im Container lauscht häufig auf einem oder mehreren Ports. Damit du vom Host darauf zugreifen kannst, veröffentlichst du diese Ports beim Erstellen des Containers. Eine weitere Möglichkeit ist, Befehle direkt im Container auszuführen, etwa zur Fehlersuche oder für einmalige Aufgaben.

Wie veröffentlichst du Ports?

Verwende die Option --publish, kurz -p, um Container-Ports auf dem Host verfügbar zu machen.

bash
# publish ports, e.g., forward container port to a port on the host system
$ docker run --publish 3000:3000 examplename/examplerepository-server:0.1.0
# or
$ docker run --publish 80:80 examplename/examplerepository-client:0.1.0
# if you run both containers, the server and the client
# and you visit the localhost:80 in your browser
# you should see the message Hello World

Im ersten Beispiel verbinden wir Port 3000 im Container mit Port 3000 auf dem Host, also deinem Entwicklungsrechner oder Server. Das Format lautet --publish <hostport>:<containerport>.

Wie greifst du auf einen laufenden Container zu?

Direkter Zugriff auf einen Container ist beispielsweise bei der Fehlersuche hilfreich.

bash
# access the container
$ docker exec --interactive --tty <container-id> <shell-command>

--interactive --tty, kurz -it, hält die Standardeingabe offen und weist ein Pseudoterminal zu. Zusammen mit einem Shell-Befehl ermöglicht das eine interaktive Shell im Container.

Du kannst alle Befehle ausführen, die im Container verfügbar sind. In einem Debian-Container kannst du beispielsweise das Verzeichnis auflisten:

bash
# list the directory inside the container
$ docker exec --interactive --tty <container-id> ls

Mit folgendem Befehl öffnest du eine Shell im Container:

bash
# SSH into the container (if the `sh` command exists in the container)
$ docker exec --interactive --tty <container-id> sh
# this will keep the connection to the container open
# and you can execute multiple commands within the container
# to exit the container, run the following
$ exit

Wie speicherst du Daten dauerhaft mit Docker-Volumes?

Daten im beschreibbaren Dateisystem eines Containers überleben dessen Entfernung nicht. In unserer Anwendung schreibt ein Klick auf den Button unter http://localhost:80 den Text „New message“ in eine JSON-Datei im Container. Wenn wir den Container stoppen und entfernen, werden auch diese Nachrichten gelöscht.

Um Daten unabhängig vom Container zu behalten, kannst du benannte Volumes oder Bind-Mounts verwenden. Benannte Volumes verwaltet Docker. Bei einem Bind-Mount legst du selbst fest, in welchem Verzeichnis auf dem Host die Daten liegen. Dafür verwenden wir beim Start die Option --volume, kurz -v.

bash
# using a named volume
# everything within this path of the container will be stored
# in a volume named <volume-name>
$ docker run --volume <volume-name>:/path/in/container <image-name>

# using a mounted volume
# everything inside the path of the container will be stored
# in the path of the host
$ docker run --volume /path/on/host:/path/in/container <image-name>

Für den Server-Container unserer Anwendung verwenden wir folgenden Befehl:

bash
# create a volume called server-volume
# we store the content of /app/build/data within our container
# on our host machine (your dev machine or your server)
docker run --volume server-volume:/app/build/data --publish 3000:3000 examplename/examplerepository-server:0.1.0

Wie listest du alle Volumes auf?

Durch Auflisten erhältst du eine Übersicht der vorhandenen Volumes.

bash
# list all volumes
$ docker volume ls

# here you will see the location where Docker will store the named volumes
# on the host machine

Fazit

Du kennst jetzt den Unterschied zwischen Dockerfile, Image und Container. Du kannst eigene Dockerfiles schreiben, daraus Images bauen, Container starten und vom Host auf sie zugreifen. Außerdem weißt du, wie du Daten unabhängig vom Lebenszyklus eines Containers speicherst.

Wenn du Unterstützung bei der Containerisierung brauchst, kontaktiere uns gerne. Oder komm für Fragen und Diskussionen in unsere neue Community – kostenlose Kekse für die ersten 42 Mitglieder, und nur noch 6 sind übrig 😱!

Komm in unsere Community

Hat dir dieser Artikel gefallen? Teile ihn mit deinen Kolleginnen, Kollegen und Freunden.

Melde dich für unseren Newsletter an!

Verpasse keine neuen Tipps, Anleitungen und Updates – melde dich jetzt für unseren Newsletter an! Wir schicken dir nur relevante und hilfreiche Informationen. Entdecke mit uns die Welt von Docker und darüber hinaus.

Mit deiner Anmeldung akzeptierst du die Datenschutzerklärung. Du kannst dich jederzeit über den Link am Ende unserer E-Mails abmelden.

Das ultimative Docker Cheat Sheet