Docker ====== In diesem ersten Bericht werden die grundlegenden Konzepte von Docker detailliert betrachtet. Zunächst werden die theoretischen Grundlagen von Docker erläutert, einschließlich der Containerisierungstechnologie, der Docker-Engine sowie wichtigen STandardbefehlen. Anschließend wird anhand eines Beispiels die Anwendung von Docker demonstriert. Hierzu wird eine Docker-Anwendung vorgestellt, die aus mehreren Containern besteht und eine Webanwendung mit einem Flask-Backend und einer MongoDB-Datenbank bereitstellt. Die Anwendung wird mithilfe eines NGINX-Reverse-Proxys bereitgestellt. Zunächst soll jedoch auf den Unterschied zwischen Docker und einer virtuellen Maschine eingegangen werden. Recap Virtuelle Maschinen ------------------------- Eine virtuelle Maschine ist eine virtuelle Darstellung oder Emulation eines physischen Computers. Die Virtualisierung ermöglicht die Erstellung mehrerer virtueller Maschinen auf einer einzigen physischen Maschine, wobei jede dieser virtuellen Maschinen über ein eigenes Betriebssystem und jeweils eigene Anwendungen verfügt. Eine VM kann nicht direkt mit einem physischen Computer (der Hardware) interagieren. Sie benötigt vielmehr eine schlanke Softwareschicht, den so genannten Hypervisor, der die Koordination zwischen ihr und der ihr zugrunde liegenden physischen Hardware übernimmt. Der Hypervisor ordnet jeder VM physische Rechenressourcen wie Prozessoren, RAM, persistenten Speicher und Speicherkapazitäten zu. Er hält jede VM von den übrigen getrennt, damit sie sich nicht gegenseitig beeinträchtigen. [ibm.com]_ **VM Funktionsweise** .. figure:: https://www.ionos.de/digitalguide/fileadmin/DigitalGuide/Screenshots_2018/DE-virtuelle-maschine.png :align: center [ionos.de]_ Jeder virtuellen Maschine liegt eine Hypervisor-Software (auch Virtual-Machine-Monitor, VMM) zugrunde. Der Hypervisor läuft als Anwendung auf dem Host-Betriebssystem (Hosted Hypervisor) oder setzt direkt auf der Hardware der physischen Maschine auf (Bare-Metal-Hypervisor) und verwaltet die vom Wirtssystem zur Verfügung gestellten Hardware-Ressourcen. Dabei erzeugt die Hypervisor-Software eine Abstraktionsschicht zwischen physischer Hardware und virtueller Maschine. Jede VM läuft isoliert vom Wirtssystem und von anderen Gastsystemen in einer eigenen virtuellen Umgebung. **VM Vorteile** - **Ressourceneinsatz und verbesserter ROI:** Da mehrere VMs auf einem einzigen physischen Computer ausgeführt werden, müssen Kunden nicht jedes Mal einen neuen Server kaufen, wenn sie ein anderes Betriebssystem ausführen möchten, und können mit jeder Hardwarekomponente, die sie bereits besitzen, eine höhere Rendite erzielen. - **Skalierung:** Mit Cloud-Computing ist es einfach, mehrere Kopien derselben virtuellen Maschine bereitzustellen, um eine Zunahme der Belastung besser bewältigen zu können. - **Portierbarkeit:** VMs können nach Bedarf zwischen den physischen Computern in einem Netz verschoben werden. Dadurch ist es möglich, Workloads auf Server zu verteilen, die über freie Rechenleistung verfügen. - **Flexibilität:** Die Erstellung einer VM ist schneller und einfacher als die Installation eines Betriebssystems auf einem physischen Server, da Sie eine VM mit bereits installiertem Betriebssystem einfach klonen können. - **Sicherheit:** Im Vergleich zu Betriebssystemen, die direkt auf der Hardware ausgeführt werden, verbessern VMs die Sicherheit in mehrfacher Hinsicht. Eine VM ist eine Datei, die von einem externen Programm auf Schadsoftware gescannt werden kann. Docker ------ Die Kernfunktionalität von Docker besteht in der Container-Virtualisierung von Anwendungen. Diese steht im Gegensatz zur Virtualisierung mit virtuellen Maschinen (VM). Mit Docker wird der Anwendungs-Code inklusive aller Abhängigkeiten in ein sogenanntes Image gepackt. Die Docker-Software führt die so verpackte Anwendung in einem Docker-Container aus. Images lassen sich zwischen Systemen bewegen und auf jedem System ausführen, auf dem Docker läuft. Wie beim Einsatz einer virtuellen Maschine (VM) liegt ein Hauptaugenmerk bei Docker-Containern auf der Isolierung der laufenden Anwendung. Anders als bei VMs wird jedoch kein komplettes Betriebssystem virtualisiert. Stattdessen weist Docker jedem Container gewisse Betriebssystem- und Hardware-Ressourcen zu. Aus einem Docker-Image lassen sich beliebig viele Container erzeugen und parallel betreiben. So werden skalierbare Cloud-Dienste realisiert. **Docker-Engine** Die Docker-Engine läuft auf einem lokalen System oder einem Server und besteht aus zwei Komponenten: - **Docker-Daemon dockerd:** Läuft dauerhaft im Hintergrund und lauscht auf Zugriffe über die Docker-Engine-API. Auf entsprechende Befehle hin verwaltet dockerd Docker-Container und weitere Docker-Objekte. - **Docker-Client docker:** Dabei handelt es sich um ein Kommandozeilenprogramm. Der Docker-Client steuert Docker-Engine und stellt Befehle für Erstellung und Aufbau von Docker-Containern sowie Erstellung, Bezug und Versionierung von Docker-Images bereit. [ionos.de]_ **Docker und VM im Vergleich** +-----+---------------------------------------------------+--------------------------------+ | | Docker | VM | +=====+===================================================+================================+ | ✓ | kein komplettes Betriebssystem | sicherer (kein gemeinsames OS) | +-----+---------------------------------------------------+--------------------------------+ | ✓ | Teilt sich Kernel mit anderen | Sandbox-Funktion | +-----+---------------------------------------------------+--------------------------------+ | ✓ | leichtgewichtig und kompakt | | +-----+---------------------------------------------------+--------------------------------+ | ✓ | startet schneller als VM | | +-----+---------------------------------------------------+--------------------------------+ | ✓ | Verbund von mehreren Containern | | +-----+---------------------------------------------------+--------------------------------+ | ✓ | mehrere Instanzen desselben Images möglich | | +-----+---------------------------------------------------+--------------------------------+ | x | Schwierige Persistenz von Daten | Belastung von Ressourcen | +-----+---------------------------------------------------+--------------------------------+ | x | Nicht alle Anwendungen profitieren von Containern | Performance | +-----+---------------------------------------------------+--------------------------------+ | x | Sicherheit (gemeinsamer Linux-Kernel) | | +-----+---------------------------------------------------+--------------------------------+ [geekflare.com]_ [open-telekom-cloud.com]_ .. index:: Docker vs Virtualisierung Dockerfile ---------- Basis eines jeden Docker Containers ist sein Image. Ein Docker-Image ist eine leichtgewichtige, ausführbare Softwareeinheit, die alles enthält, was benötigt wird, um eine bestimmte Anwendung oder Umgebung auszuführen. Es enthält das Betriebssystem, die Laufzeit, Bibliotheken, Abhängigkeiten und die Anwendung selbst. Ein Docker-Image wird normalerweise von einer sogenannten Dockerfile-Datei erstellt, die die Anweisungen zum Zusammenstellen des Images enthält. Dies kann beispielsweise das Herunterladen von Basisimages, das Installieren von Softwarepaketen, das Kopieren von Dateien und das Festlegen von Umgebungsvariablen umfassen. .. code-block:: dockerfile # Base stage FROM node:18.16.0 as base WORKDIR /app COPY src ./src COPY db ./db COPY package*.json ./ # install dependencies needed for image processing RUN apt-get update && \ apt-get install -y graphicsmagick RUN apt-get install -y ghostscript RUN npm install --only=production # Development stage FROM base as development ENV NODE_ENV=development RUN npm install --only=development CMD ["npm", "run", "dev"] # Production stage FROM base as production ENV NODE_ENV=production CMD ["npm", "start"] Dieses Dockerfile wird verwendet um ein Image für ein App Backend zu erstellen. Es nutzt als Basis Node.js welches dann mit den Backspezifieschen Daten erweitert wird. Docker Image ------------ Bei einem Docker-Image handelt es sich um eine schreibgeschützte Vorlage zum Erzeugen eines oder mehrerer identischer Container. Docker-Images werden eingesetzt, um Anwendungen zu bündeln und auszuliefern. Für den Bezug von Docker-Images kommen verschiedene Repositories zum Einsatz. Es gibt sowohl öffentliche als auch nichtöffentliche Repositories. Über die Docker-Kommandos 'docker pull' und 'docker push' wird ein Image von einem Repository bezogen bzw. dort abgelegt. Docker-Images sind in Schichten (Layers) aufgebaut. Jedes Layer repräsentiert eine spezifische Änderung am Image. Damit ergibt sich eine durchgehende Versionierung der Images, was ein Rollback zu einem früheren Zustand ermöglicht. Zur Erzeugung eines neuen Images kann ein existierendes Image als Grundlage genutzt werden. Docker Container ---------------- Ein Docker Container ist eine Einheit, die eine Anwendung und alle ihre Abhängigkeiten enthält. Er isoliert die Software von der Umgebung und garantiert, dass sie auf jedem System gleich funktioniert. Dies ist unabhängig vom Betriebssystem, der Hardware oder der Infrastruktur, auf der die Container laufen. [Container]_ **Container-Betriebssystem und Union File System** Ein Docker-Container enthält ein minimales Betriebssystem und ein Union File System. Das Betriebssystem ist in der Regel eine lightweight Version eines Linux-Kernels. Wie oben beschrieben, besteht ein Docker-Image besteht aus mehreren schreibgeschützten Layers. Wenn ein Image in einem Container gestartet wird, fügt Docker eine neues beschreibbares Layer an der Spitze des Stapels hinzu. Dies wird als Union File System bezeichnet. Jedes Mal, wenn eine Datei geändert wird, erstellt Docker eine Kopie der Datei von den schreibgeschützten Layers bis in das oberste beschreibbare Layer. Dadurch bleibt die ursprüngliche (schreibgeschützte) Datei unverändert. Wenn ein Container gelöscht wird, geht das obere beschreibbare Layer verloren. Das bedeutet, dass alle Änderungen, die nach dem Start des Containers vorgenommen wurden, nicht mehr vorhanden sind. Um Daten zu persistieren, auch wenn ein Docker-Container gelöscht wird, können Volumes verwendet werden. **Volumes** Volumes sind der bevorzugte Mechanismus zum Persistieren von Daten, die von Docker-Containern erzeugt und verwendet werden. Sie existieren unabhängig vom Lebenszyklus des Containers, was bedeutet, dass Volumes existieren können, auch wenn der Container gelöscht wird. Außerdem sind Volumes eine praktische Möglichkeit, Daten zwischen dem Host und dem Container auszutauschen. Sie können aber auch zwischen Containern geteilt werden. Die Volumes kännen mit Docker CLI-Befehlen oder der Docker API verwaltet werden. Es gibt Befehle zum Erstellen, Auflisten, Untersuchen und Entfernen von Volumes. **Ports und Umgebungsvariablen** Ports ermöglichen die Kommunikation zwischen dem Docker-Container und der Außenwelt. Docker-Container können Ports öffnen, um Netzwerkanfragen zu empfangen und auf sie zu reagieren. Umgebungsvariablen können verwendet werden, um konfigurierbare Werte an den Container zu übergeben. [ContainerComponents]_ Container Netzwerke ------------------- Container können miteinander kommunizieren, indem sie in einem gemeinsamen Netzwerk verbunden werden. Dieses Netzwerk ist ein virtuelles Netzwerk, das von Docker verwaltet wird. Sobald sich Container im selben Netz befinden, können sie über die IP-Adressen der Container oder auch deren Namen miteinander kommunizieren. Da die Ports eines Containers standarmäßig nicht nach außen freigegeben sind, müssen diese explizit freigegeben werden. Dies kann durch die Angabe von Ports im Dockerfile oder durch die Verwendung von Flags der Docker-CLI-Befehle (``--publish`` bzw. ``-p``) erfolgen. Dadurch wird eine Firewall-Regel auf dem Host erstellt, die den eingehenden Datenverkehr auf den angegebenen Port an den Container weiterleitet. [DockerNetworks]_ Docker Compose -------------- Mehrere Container miteinander zu vernetzen und zu organisieren kann schnell komplex werden. Docker Compose stellt ein Konzept vor, welches das Management von mehreren Containern einfach gestaltet. Neben Containers können in einer Compose-File auch Netzwerke, Speicherumgebungen oder andere Konfigurationen angegeben werden. Mithilfe nur einer YAML-Datei kann man mehrere Regeln für verschiedene Container definieren. In dieser Datei kann man beispielsweise das Image, das verwendete Netzwerk oder Ressourcenlimitierungen angeben. Zusätzlich ermöglicht Docker Compose auch das direkte Anlegen von Umgebungsvariablen. [ComposeHow]_ Beispielsweise können für Container, welche für Datenbankanwendungen verwendet werden, direkt der Datenbankname, sowie der Username angegeben werden: .. code-block:: yaml services: database: image: "postgres:${POSTGRES_VERSION}" environment: DB: mydb USER: "${USER}" [ComposeIntro]_ Zusätzlich ermöglicht Docker Compose das einfache teilen und dublizieren von Konfigurationen. Diese Konfigurationen werden beim Start eines Services gecached. Somit kann ein Container bei einem Neustart sehr schnell gestartet werden. [DockerWhy]_ Beispiel Pyramid App --------------------- Als Beispeil für den Produktiven Einsatz verwenden wir die Docker Scripts für die Pyramid App und dessen Backend. Anhand des bereits gezeigten Dockerfiles und diesem docker-compose.yml script kann die Funktionsweise von Docker und der Vorteil seiner modularen Struktur gezeigt werden. .. code-block:: yaml version: "3" services: pyramid-api: build: context: . target: development container_name: "pyramid-api" restart: always expose: - "3000" networks: - api-backend - nginx-connector volumes: - api_data:/app/src - api_db:/app/db environment: MONGODB_USERNAME: ${MONGODB_USERNAME} MONGODB_PASSWORD: ${MONGODB_PASSWORD} mongo: image: mongo:6.0 container_name: "pyramid-mongodb" restart: always networks: - api-backend expose: - 27017/tcp volumes: - mongodb:/data/db environment: MONGO_INITDB_ROOT_USERNAME: ${MONGODB_USERNAME} MONGO_INITDB_ROOT_PASSWORD: ${MONGODB_PASSWORD} MONGO_INITDB_DATABASE: admin nginx_reverse_proxy: image: 'jc21/nginx-proxy-manager:2.11.1' container_name: "pyramid-nginx" restart: always ports: - "80:80" - "443:443" networks: - nginx-connector volumes: - nginx_data:/data - nginx_ssl:/etc/letsencrypt volumes: mongodb: api_db: api_data: nginx_data: nginx_ssl: networks: api-backend: nginx-connector: Das Backend dieser App besteht aus 3 Container, der Api mit der Logik, einer MongoDB welche die Daten Speicher und einem Nginx Reverse Proxy der die SSL Verschlüsselung übernimmt und die Anfragen an den richtigen API Container weiterleitet. Die tatsächlichen Daten der Container werden in Volumes gespeichert. Die Container untereinander sind mittels zwei verschiedenen Netzwerken verbunden, um die Datensicherheit weiter zu erhöhen. Anwendung der Docker-Grundlagen anhand eines Beispiels ------------------------------------------------------ Um die theoretischen Konzepte von Docker in der Praxis zu demonstrieren, haben wir eine Beispielanwendung entwickelt, die aus drei Containern besteht: einem Flask-Backend, einer MongoDB-Datenbank und einem NGINX-Reverse-Proxy. Die Anwendung dient als einfaches Setup, um die Kommunikation zwischen Containern und die Verwendung von Docker-Compose zu veranschaulichen. **Flask-Backend** Der Flask-Container dient als Backend für die bereitgestellte Anwendung. Dieser Container wird aus einem individuell definierten Dockerfile erstellt, das auf einem schlanken Python 3.10-Image basiert und in unserem `Dockerfile Repository `_ zu finden ist. Das Dockerfile enthält die notwendigen Anweisungen zum Erstellen des Images, wie das Installieren aller benötigten Python Pakete aus der requirements.txt-Datei und das Kopieren des Anwendungs-Codes in den Container. Der Flask-Container wird im Docker-Compose-File als Service definiert und mit dem Netzwerk *api-backend* verbunden, um mit dem nginx-Proxy kommunizieren zu können, sowie dem Netzwerk *mongodb-network*, um auf die MongoDB-Datenbank zugreifen zu können. Um den Flask-Server richtig zu konfigurieren, werden Umgebungsvariablen verwendet, die im Docker-Compose-File definiert sind. Diese Variablen enthalten die Verbindungsdaten zur MongoDB-Datenbank und den Port, auf dem der Flask-Server lauscht. Dieser Port wird im Docker-Compose-File freigegeben, um den Zugriff auf den Server seitens des Proxys zu ermöglichen ohne den Port jedoch auf dem Host-System zu veröffentlichen. Zusätzlich werden lokale Verzeichnisse auf Volumes im Container gemappt. In diesem Fall wird das lokale Verzeichnis ./flask in das Container-Verzeichnis /app gemountet. Um den Container ordnungsgemäß zu beenden, wird für Flask ein spezielles Stop-Signal benötigt, *SIGINT*. Folgender Code zeigt den Ausschnitt aus dem Docker-Compose-File, der den Flask-Service definiert: (Das gesamte Docker-Compose-File ist im `Compose Repository `_ verfügbar) .. code-block:: yaml services: flask-backend: build: context: ./flask container_name: "flask-backend" stop_signal: SIGINT environment: - FLASK_SERVER_PORT=9090 - MONGODB_URI=mongodb://mongo:27017 restart: always networks: - api-backend - mongodb-network expose: - 9090 volumes: - ./flask:/app depends_on: - mongo **MongoDB-Datenbank** Der Datenbank-Container basiert auf einem offiziellen MongoDB-Image, das von Docker-Hub bezogen wird. Der Container wird nur mit dem Netzwerk *mongodb-network* verbunden, sodass der Flask-Container auf die Datenbank zugreifen kann, aber vom nginx-Proxy isoliert ist. Auch für diesen Container wird ein Volume angelegt. Im folgenden ist der entsprechende Abschnitt aus dem Docker-Compose-File zu sehen: .. code-block:: yaml mongo: image: mongo:6.0 container_name: "mongodb" restart: always networks: - mongodb-network volumes: - ./mongodb_data:/data/db **NGINX-Reverse-Proxy** Der NGINX-Reverse-Proxy-Container wird aus einem offiziellen NGINX-Image erstellt und dient als Schnittstelle zwischen dem Backend und der Außenwelt. Der Proxy leitet Anfragen an den Flask-Server weiter und übernimmt die SSL-Verschlüsselung. Die Flask-Website ist über Port 80 der Proxy-Adresse erreichbar. Der NGINX-Container wird mit dem Netzwerk *api-backend* verbunden, um mit dem Flask-Backend zu kommunizieren. .. code-block:: yaml nginx_reverse_proxy: image: nginx container_name: "nginx" restart: always ports: - "80:80" networks: - api-backend environment: - FLASK_SERVER_ADDR=flask-backend:9090 command: /bin/bash -c "envsubst < /tmp/nginx.conf > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'" volumes: - ./nginx/nginx.conf:/tmp/nginx.conf depends_on: - flask-backend Um die Konfiguration des NGINX-Containers zu vereinfachen, wurde ein Konfigurationsfile erstellt, welches mittels eines volumes in den Container gemountet wird. Um die Konfiguration vorzunehmen, muss das File zunächst durch die default-Konfiguration von NGINX ersetzt werden, was durch den command-Befehl im Compose-File ausgeführt wird. Anschließend wird der NGINX-Server gestartet. ``daemon off`` verhindert, dass der NGINX-Server nur im Hintergrund gestartet wird, was für die Verwendung in einem Container nicht sinnvoll ist. Die NGINX-Konfiguration sieht wie folgt aus: .. code-block:: nginx server { listen 80; server_name localhost; location / { proxy_pass http://$FLASK_SERVER_ADDR; } include /etc/nginx/extra-conf.d/*.conf; } Daraus wird ersichtlich, dass der NGINX-Server auf Port 80 lauscht und Anfragen an den Flask-Server weiterleitet. [NGINX]_ **Test der Anwendung** Um die Anwendung zu testen, kann das Docker-Compose-File mit dem Befehl ``docker-compose up`` gestartet werden. Die Flask-Website ist dann über die Adresse http://localhost erreichbar und sollte die Meldung 'MongoDB is available' anzeigen. Um die Anwendung zu beenden, kann der Befehl ``docker-compose down`` verwendet werden. Der Testcode der Flask-Anwendung ist in unserem `Repository `_ einsehbar. Dort ist eine simple Methode implementiert, die den Status des MongoDB-Servers überprüft und eine entsprechende Meldung zurückgibt. Zusammenfassend zeigt dieses Beispiel, wie Docker-Container miteinander kommunizieren und wie Docker-Compose verwendet wird, um mehrere Container zu verwalten und zu orchestrieren. [DockerCompose]_ Standardbefehle --------------- **Docker Repositories** - ``docker pull :``: Lädt ein Image von einem Repository herunter. - ``docker search ``: Sucht nach einem Image in einem Repository (standardmäßig DockerHub). **Images** - ``docker images``: Zeigt alle Images an. - ``docker rmi :``: Löscht ein Image. - ``docker build -t : .``: Erstellt ein Image aus einem Dockerfile (muss im aktuellen Verzeichnis liegen). **Container** - ``docker run :``: Startet einen Container aus einem Image. - ``-d``: Startet den Container im Hintergrund ("detached mode"). - ``-p :``: Veröffentlicht einen Port des Containers auf den Host. - ``-v :``: Bindet ein Volume. - ``-it``: Startet den Container im interaktiven Modus. - ``--name ``: Gibt dem Container einen Namen. - ``--network ``: Verbindet den Container mit einem Netzwerk. - ``--gpus=all``: Aktiviert GPU-Unterstützung. - ``--network=host``: Verwendet das Host-Netzwerk. - ``docker stop ``: Stoppt einen Container. - ``docker exec -it ``: Führt ein Kommando in einem laufenden Container aus. - ``docker rm ``: Löscht einen Container. - ``docker ps``: Zeigt alle **laufenden** Container an. - ``docker ps -a``: Zeigt alle Container an. **Netzwerke** - ``docker network create ``: Erstellt ein Netzwerk. - ``docker network ls``: Zeigt alle Netzwerke an. **Volumes** - ``docker volume create ``: Erstellt ein Volume. - ``docker volume ls``: Zeigt alle Volumes an. - ``docker volume rm ``: Löscht ein Volume. **Docker Compose** - ``docker-compose up``: Startet alle Services in einem Docker-Compose-File. - ``docker-compose down``: Stoppt alle Services in einem Docker-Compose-File. - ``docker-compose ps``: Zeigt alle Services in einem Docker-Compose-File an. Literaturangaben ---------------- .. [ibm.com] IBM Homepage https://www.ibm.com/de-de/topics/virtual-machines (besucht am 30.3.2024) .. [ionos.de] Ionos Homepage https://www.ionos.de/digitalguide/server/knowhow/virtuelle-maschinen/ (besucht am 30.3.2024) .. [geekflare.com] Geekflare Homepage https://geekflare.com/de/docker-vs-virtual-machine/ (besucht am 30.3.2024) .. [open-telekom-cloud.com] Open-Telekom-cloude Homepage https://www.open-telekom-cloud.com/de/blog/cloud-computing/container-vs-vm (besucht am 30.3.2024) .. [Container] Docker Hompage https://www.docker.com/resources/what-container/ (besucht am 02.04.2024) .. [ContainerComponents] Ionos Homepage https://www.ionos.com/digitalguide/server/know-how/docker-container/ (besucht am 02.04.2024) .. [DockerNetworks] Docker Docs https://www.docs.docker.com/network/ (besuscht am 02.04.2024) .. [ComposeHow] Docker Docs https://docs.docker.com/compose/compose-application-model/ (besucht am 28.03.2024) .. [ComposeIntro] Baeladung Homepage https://www.baeldung.com/ops/docker-compose (besucht am 28.03.2024) .. [DockerWhy] Docker Docs https://docs.docker.com/compose/intro/features-uses/ (besucht am 28.03.2024) .. [NGINX] Archive Homepage (patricksoftwareblog) https://web.archive.org/web/20230321180419/https://www.patricksoftwareblog.com/how-to-configure-nginx-for-a-flask-web-application/ (besucht am 02.04.2024) .. [DockerCompose] Docker Docs https://docs.docker.com/get-started/08_using_compose/ (besucht am 02.04.2024)