Nun, im Grunde genommen ist ein laufender Container nichts anderes als ein Prozess in einem chroot. Wenn du Docker das erste Mal benutzt, musst du dir entweder ein Image bauen oder eins herunterladen. Herunterladen kannst du die mit:
Bash:
docker image pull nginx:latest
Gibst du keine Domain an, wird "docker.io" genommen. Gibst du kein Tag an, wird "latest" genommen. Nach dem Download liegt das Image dann irgendwo in "/var/lib/docker", allerdings nicht direkt in einem Verzeichnis, sondern eventuell über mehrere Verzeichnisse verteilt, die dann übereinander gelegt werden, wenn du aus dem Image einen Container starten willst. Docker layert halt die verschiedenen Images, damit sie wiederverwendbar sind. Das "nginx"-Image basiert z.B. auf Debian. Der Debian-Layer kann dann über mehrere Images wiederverwendet werden.
Wenn du jetzt eben aus diesem nginx-Image einen Container erzeugen willst, machst du das entweder mit "docker container create" oder "docker container run". Der "create"-Befehl erstellt den Container, aber startet ihn nicht. Du musst ihn dann nachträglich mit "docker container start" starten. Der "run"-Befehl erstellt ihn und startet ihn auch sofort. Das ist meistens das, was du willst:
Bash:
docker container run --name my-nginx nginx:latest
Idealerweise gibst du den Containern immer mit "--name" einen Namen. Ansonsten generiert dir Docker einen Namen, den man sich logischerweise schlechter merken kann. Was da am Ende wirklich in dem Container ausgeführt wird, bestimmt das Image. Docker hat einen Entrypoint und einen CMD. Der Entrypoint ist nicht selten stumpf "/bin/sh -c", der CMD im Falle des nginx-Containers "nginx". Heißt wenn du den Container startest, führt Docker "/bin/sh -c nginx" im chroot des Containers aus, wodurch du einen sandboxed nginx erhältst. Für eine Übersicht über alle deine Container, nutzt du:
Um auch gestoppte Container zu sehen, musst du noch ein "-a" anhängen. Stoppen kannst du dann einen Container mit "docker container stop" und daraufhin dann "docker container rm", damit er nicht nur gestoppt ist, sondern auch entfernt wird:
Bash:
docker container stop my-nginx
docker container rm my-nginx
Wenn du versuchst hast im Browser "localhost" aufzurufen, wirst du bemerken, dass du nicht auf den nginx zugreifen konntest. Das liegt daran, dass du jeden Port eines Containers explizit freigeben musst. Tust du das nicht, kann der Container nur mit anderen Containern im virtuellen Netzwerk kommunizieren:
Bash:
docker container run --name my-nginx -p 8080:80 nginx:latest
Jetzt kannst du auf den nginx unter "localhost:8080" zugreifen.
Es besteht auch die Möglichkeit in einem bereits laufenden Container einen weiteren Prozess auszuführen. Das geht mit "docker container exec". Möchtest du zum Beispiel in deinem nginx-Container eine Bash starten, kannst du das hier machen:
Bash:
docker container exec -it my-nginx bash
Das "-i" steht für "--interactive", das "-t" für "--tty". Das Interactive-Flag brauchst du, wenn der Befehl Daten via stdin erhalten soll, im Falle der Bash die Tastatureingaben. Das TTY-Flag ist dafür gedacht eine Pseudo-TTY anzuhängen.
Möchtest du jetzt zwei Container miteinander kommunizieren lassen, müssen sie im selben Netzwerk sein. Wir stoppen also erstmal unseren "my-nginx"-Container und erstellen ein Netzwerk:
Bash:
docker container stop my-nginx
docker container rm my-nginx
docker network create my-network
An dieses Netzwerk können wir jetzt neue Container anschließen, zum Beispiel:
Bash:
docker container run --name my-nginx --network my-network nginx:latest
Wir können jetzt einen weiteren Container starten, z.B. einen Debian-Container, diesen in das gleiche Netzwerk einbinden und dann z.B. via curl auf den nginx-Container zugreifen:
Bash:
docker container run --rm debian:latest curl http://my-nginx/
Der Hostname ist immer der Name des Containers. Wenn du einen alternativen Namen willst, kannst du mit "--network-alias" einen beim Starten des nginx-Containers festlegen. Wir benutzen hier in dem Befehl auch ein "--rm". Damit wird der Container automatisch gelöscht sobald er gestoppt wird. Da der Container nur so lange läuft wie das "curl" ist er also sofort wieder weg und wir müssen ihm nicht mit "docker container rm" hinterher räumen.
Das letzte wichtige Konzept von Docker sind Bind-Mounts und Volumes. Bind-Mounts sind nichts Anderes als in den Container reingemountete Dateien. Sagen wir einfach mal du hast im aktuellen Verzeichnis deines Hosts eine eigene "nginx.conf" liegen und außerdem deine Webapplikation in einem Verzeichnis "app". Du möchtest du die "nginx.conf" im Container damit überschreiben und außerdem deine App in den Container nach "/var/www" mounten. Dann machst du das z.B. so:
Bash:
docker container run \
--name my-nginx \
--rm \
-p 80:80 \
-v "${PWD}"/nginx.conf:/etc/nginx/nginx.conf:ro \
-v "${PWD}"/app:/var/www:ro \
nginx:latest
Das "-v" besteht aus dein Blöcken, per ":" getrennt. Der erste Block ist die Quelle. Docker ist hier leider etwas... scheiße und will zwingend absolute Pfade. Wir müssen hier also mit der Umgebungsvariable "$PWD" das aktuelle Verzeichnis voranstellen. Der zweite Block ist dann das Ziel im Container. Besteht dieses Ziel bereits, wird es überschrieben. Der dritte Block ist optional und beinhaltet Optionen für den Mount, z.B. "ro" für read-only. Denn der Container muss auf die "nginx.conf" oder in dein "app"-Verzeichnisses nicht schreiben können. "rw" ist nämlich der Default.
Ein Volume mag zwar auf den ersten Blick identisch zu einem Bind-Mount sein, ist es aber nicht. Ein Bind-Mount nimmt eine lokale Datei und loop-mounted sie über eine Datei im Container. Die Datei hat dann im Container den gleichen Owner wie auf dem lokalen Host, also höchstwahrscheinlich den im Container nicht existenten "1000:1000". Ein Volume weist Docker jedoch an ein bestimmtes Verzeichnis im Container zu persistieren. Ist diese Verzeichnis nicht leer, kopiert Docker den Inhalt des Verzeichnisses in das Volume. Ein Container ist per default immutable. Egal was du darin machst, es ist weg sobald der Container beendet wird. Sagen wir einfach mal du hast eine Applikation in einem Container laufen, mit der ein User Bilder hochladen kann. Diese Bilder speichert deine Applikation in "/media". Du möchtest natürlich nicht, dass bei einem Server-Neustart (und damit natürlich auch Container-Neustart) die Bilder weg sind. Also erstellst du ein Volume für dein Verzeichnis:
Bash:
docker volume create my-nginx-media
docker container run \
--name my-nginx \
--rm \
-p 80:80 \
-v "${PWD}"/nginx.conf:/etc/nginx/nginx.conf:ro \
-v "${PWD}"/app:/var/www:ro \
-v my-nginx-media:/media \
nginx:latest
Damit Docker weiß, ob ein Volume oder ein Bind-Mount gemeint ist, prüft er, ob ein Slash in der Quelle vorkommt. "my-nginx-media" enthält kein Slash, also wird ein Volume benutzt. Docker speichert diese Volumes dann in "/var/lib/docker/volumes". Es ist aber nicht gedacht, dass man manuell in diesen Verzeichnissen rumspielt, auch wenn man könnte. Volumes behandelt Docker als wichtig. Befehle in Docker löschen dir niemals ein Volume zusammen mit einem Container, es sei denn du willst es so. So ist es also auch Best Practive eine MySQL-Datenbank in einem Volume speichern, z.B. mit "-v mysql-data:/var/lib/mysql".
Cooler wäre es bei einer echten Produktivanwendung jetzt natürlich wenn deine Applikation und deine "nginx.conf" nicht immer gebindmounted sein müssen, sondern ebenfalls mit im Image drin sind. Dafür erstellst du dir eine "Dockerfile" mit folgendem Inhalt:
Code:
FROM nginx:latest
COPY ./nginx.conf /etc/nginx/nginx.conf
COPY ./app /var/www
Das "FROM" sagt Docker auf welchem Image unser Image basieren soll. "FROM scratch" ist zwar auch möglich, macht aber selten Sinn, da wir dann selber für unser Dateisystem sorgen müssen, z.B. mit "debootstrap" (Debian) oder "pacstrap" (Arch Linux). Das "COPY" kopiert dann Dateien vom Host in das Image. Wenn du Befehle im Container ausführen möchtest, kannst du das mit "RUN", z.B.:
Code:
FROM nginx:latest
RUN apt-get update && apt-get install -y foobar
Mit dieser "Dockerfile" kannst du dann ein Image bauen:
Bash:
docker image build -t my-nginx:v1 .
Der Punkt am Ende ist der Kontext, in diesem Falle also das aktuelle Verzeichnis. Im Kontext wird nach einer Dockerfile gesucht und auf den Kontext beziehen sich auch alle relativen Host-Pfade in der "Dockerfile". Das "-t" gibt den Tag an, den das fertige Image erhalten soll. Mit diesem Tag können wir das Image dann nachher als Container benutzen, z.B.:
Bash:
docker container run \
--name my-nginx \
--rm \
-p 80:80 \
-v my-nginx-media:/media \
my-nginx:v1
Unsere Bind-Mounts müssen wir nun nicht mehr angeben. Immerhin liegen die Dateien jetzt mit im Container.
Noch als Ergänzung:
- In einem Container sollte immer nur ein Prozess laufen. Manche Leute installieren einen MySQL und PHP im nginx-Container und lassen dann alle drei zusammen mit supervisord oder systemd laufen. Ist scheiße, lass das. Erstell lieber ein Netzwerk und starte drei dedizierte Container für nginx, PHP und MySQL, die alle im gleichen Netzwerk sind. Bonuspunkte für ein Netzwerk für nginx <-> PHP und eins für PHP <-> MySQL. Aber man kann auch overengineeren...
- Speichere keine Secrets im Container. Wenn du Passwörter übergeben willst, kannst du auch einfach Umgebungsvariablen benutzen z.B. mit "docker container run -e FOO=bar".
- Benutze am Besten nie "latest", sondern immer Versionen, z.B. "1.24.0", sonst weißt du nie was für ne Version von nginx/PHP/MySQL du bekommst, und das kann uncool werden.
- Nutze Docker Compose. Compose ist große Liebe. Kubernetes ist für 99% der Anwendungen absolut overkill. Docker Compose aber baut dir aus seiner YAML-Datei die entsprechenden Docker-Befehle zusammen und fährt dir z.B. auch Container runter, die deine Applikation nicht mehr braucht. Docker Compose ist genauso einfach. Wenn du willst, kann ich dir da auch eine Beispieldatei mal schicken.