Einführung in Docker
Von Julian Sauer
- 14 Minuten - 2976 WörterLaut einer Stackoverflow Umfrage von 2021 ist Docker mit ungefähr 50% eins der am meistbenutzten Tools in Softwareprojekten. Es bildet eine Abstraktionsschicht zwischen Betriebssystemen und Softwarekomponenten wie Datenbanken, Webservern, oder Backends. Im Mittelpunkt von Docker stehen die sogenannten Container. Angelehnt an Seefracht Container bieten sie eine standardisierte Umgebung, in der Software ausgeliefert und ausgeführt werden kann. Ein manuelles Installieren einzelner Komponenten und Abhängigkeiten entfällt.
Der Beitrag richtet sich an jene, die noch keine Erfahrung mit Docker haben. Ein paar Grundkenntnisse von Linux-Befehle sind allerdings hilfreich. Weiter unten gibts ein Glossar, in dem einige der technischen Begriffe erklärt werden.
Inhaltsverzeichnis
- Wofür brauche ich Docker
- Die wichtigsten Komponenten von Docker
- Hello World
- Eigene Docker Images
- Persistente Daten
- Architektur & Zusammenfassung
- Die wichtigsten Befehle
- Glossar
Wofür brauche ich Docker?
Stell dir vor, du arbeitest zusammen mit anderen Entwicklern an einer Website auf der sich Benutzer anmelden können, um Bilder von Schwimmnudeln hochzuladen und mit anderen Nutzern zu teilen. Neben der Website, die die Benutzer sehen, gibt es ein zentrales Backend, das die Bilder verwaltet. Es überprüft, ob die Bilder auch wirklich Schwimmnudeln enthalten und speichert sie dann in einer Datenbank wie PostgreSQL, um sie später zum Teilen auf Social Media abrufen zu können. Neben diesen 3 Komponenten ist außerdem ein Keycloak angebunden, er übernimmt die Benutzer- und Rechteverwaltung. Dadurch können sich Benutzer mit Namen + Passwort anmelden und es wird beispielsweise sichergestellt, dass jeder nur seine eigenen Bilder bearbeiten kann.
Der Tech Stack ist überschaubar. Wenn aber ein neuer Entwickler dazu stößt, um etwa Änderungen an der Benutzeroberfläche vorzunehmen, wird er sich zunächst durch eine Installationsanleitung wühlen müssen. Verlässt ein Entwickler das Projekt, möchte er vermutlich alles wieder deinstallieren.
Außerdem ist die Benutzeranfrage in letzter Zeit stark gestiegen. Datenbank, Keycloak und Backend laufen auf dem gleichen Server, der auch die Website ausliefert. Klar, skalieren ist immer schwierig. Aber zumindest die 4 Komponenten auf eigene Server auszulagern, wäre ohne größere Anpassungen in der Software möglich. Natürlich müssen dafür alle nötigen Komponenten mit ihren jeweiligen Abhängigkeiten auf den neuen Servern installiert werden. Vielleicht sollte bei der Gelegenheit ein Cache wie Redis eingebaut werden, in dem beliebte Bilder gespeichert werden. Damit könnte die Wartezeit bei Benutzern reduziert werden.
In Version 2.0 soll es außerdem möglich sein, nach Bildern anhand von Tags oder Titeln zu suchen. Vielleicht können diese Metadaten mit ElasticSearch indiziert werden, um sie leichter und schneller durchsuchen zu können. Und dann muss das ganze natürlich auf die Server gebracht werden sowie lokal bei allen Entwicklern eingerichtet werden. Teilweise gibt es Skripte, um einzelne Komponenten einzurichten, lokale Datenbanken mit Beispieldaten zu füllen oder Testbenutzer im Keycloak anzulegen. Aber da einige Entwickler Windows, andere Mac benutzen und die Server linuxbasiert sind, funktionieren sie nicht überall gleich. Die Installationsanleitung ist mittlerweile länger als der Quellcode und ein Entwickler ist ausschließlich dafür abgestellt, diese zu pflegen und neuen Kollegen beim Einrichten zu helfen. Eine neue Abstraktionsschicht muss her…
Die wichtigsten Komponenten von Docker
Wie eingangs erwähnt, spielen Container eine wichtige Rolle, denn in ihnen läuft die Software, die wir betreiben wollen. Um einen passenden Container ans Laufen zu kriegen, startet man mit einem Dockerfile. Es ist ein Textdokument und vergleichbar mit einer Installationsanleitung. Darin sind alle Schritte festgehalten, die ein Entwickler/Administrator sonst per Hand durchführt, um eine Anwendung auf einem System zu installieren (Pakete und Abhängigkeiten herunterladen, entpacken, einen Benutzer anlegen, der die Anwendung ausführt usw.).
Aus einem Dockerfile kann anschließend ein Image erzeugt werden. Es ist ein Speicherabbild von dem Zustand des Systems, nachdem dort alle Anweisungen aus dem Dockerfile durchlaufen sind. Ein Image kann daraufhin verschickt oder gestartet werden. Die laufenden Instanzen von Images sind Container. Theoretisch kann der Zustand von einem laufenden Container wieder als Image auf dem Dateisystem gespeichert werden, der Weg über Dockerfiles ist jedoch leichter zu wiederholen und nachzuvollziehen.
Wir starten links mit einem Dockerfile. In ihm könnte z.B. stehen, dass wir die Linux-Distribution Ubuntu als Basis verwenden wollen und in dieser unsere Datenbank herunterladen und installieren wollen. Werden die Schritte aus dem Dockerfile durchlaufen, erhalten wir unser Image. Anschließend kann eine beliebige Anzahl an Containern von diesem Image gestartet werden, um laufende Instanzen unserer Datenbank zu erhalten und sie tatsächlich verwenden zu können. Da eine Datenbank zustandsbehaftet ist, ist das zwar nur begrenzt sinnvoll. Vielleicht können wir so aber zwei getrennte Instanzen für unser Test- und Produktivsystem laufen lassen, die beide auf der gleichen Version der Datenbank beruhen und sich nur in den Daten unterscheiden.
Im Fall von gängigen Anwendungen wie PostgreSQL, Keycloak oder Redis sind wir natürlich nicht die ersten, die dafür ein Image benötigen. Oft gibt es deshalb bereits offizielle Images, die wir von Diensten wie Docker Hub herunterladen können. Schauen wir uns deshalb im nächsten Abschnitt zunächst an, wie man ein Image herunterladen und starten kann, bevor wir im darauf folgenden Abschnitt unser eigenes Image aus einem Dockerfile erstellen.
Hello World
Zunächst muss Docker Desktop installiert werden. Bei Windows Home Edition fehlt Hyper-V, in dem Fall kann seit einiger Zeit die WSL 2 Version verwendet werden kann.
Wir starten unseren ersten Container mit docker run <image>:<version>
:
▶ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
2db29710123e: Pull complete
Digest: sha256:37a0b92b08d4919615c3ee023f7ddb068d12b8387475d64c622ac30f45c29c51
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/get-started/
Die Version des Images haben wir weggelassen, in dem Fall wird automatisch latest
verwendet. In der Konsolenausgabe sehen wir, dass das Image lokal nicht existiert und deshalb automatisch von Docker Hub heruntergeladen wird. Anschließend wird es gestartet und begrüßt uns mit einer Hello World-Nachricht. Außerdem wird vorgeschlagen, etwas mehr ambitious zu sein:
▶ docker run -it ubuntu bash
Unable to find image 'ubuntu:latest' locally
latest: Pulling from library/ubuntu
7b1a6ab2e44d: Pull complete
Digest: sha256:626ffe58f6e7566e00254b638eb7e0f3b11d4da9675088f4781a50ae288f3322
Status: Downloaded newer image for ubuntu:latest
root@c4d9ecc59f98:/# ls
bin boot dev etc home lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp usr var
root@c4d9ecc59f98:/# exit
exit
Die Flags des run-Befehls -it
geben an, dass wir den Container interaktiv starten und unsere Ein- und Ausgabe auf die des Containers umlenken wollen. Wir landen hier also in der Konsole eines Ubuntu-Containers und können z.B. ls
eingeben, um uns die Dateien und Ordner im aktuellen Verzeichnis aufzulisten. Mit exit
kommen wir wieder zurück.
Hinter dem Namen des Images haben wir mit bash
angegeben, dass wir im Container eine Bash starten wollen. Alternativ können wir z.B. auch ls
angeben, um uns die Dateien im Container aufzulisten:
▶ docker run -it ubuntu ls
bin dev home lib32 libx32 mnt proc run srv tmp var
boot etc lib lib64 media opt root sbin sys usr
Eigene Docker Images
Für unser Anfangsbeispiel können wir bereits einen Großteil der Komponenten wie unsere Datenbank oder Keycloak durch offizielle Images abbilden. Da wir auf unseren Servern selbst keine Software außer Docker installieren möchten, müssen wir zusätzlich unsere selbst entwickelte Software (Front- und Backend) in ein Docker Image verpacken. Als Beispiel-Backend verwenden wir eine Rest-API, die einen Gruß zurücksendet, wenn der /greet
-Endpunkt aufgerufen wird. Über den Paramter name
kann die Nachricht personalsiert werden. Das Skript dafür liegt auf Github:
Starten wir es zunächst in der Konsole, können wir es lokal unter dieser Adresse im Browser testen:
▶ python3 greetings-api.py
* Serving Flask app "greetings-api" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
Das gleiche Verhalten wollen wir nun über einen Docker Container erreichen. Also erstellen wir eine Datei mit dem Namen Dockerfile, damit wir davon ein Image erzeugen und es anschließend starten können:
FROM python:3.9
RUN git clone https://gist.github.com/JulianSauer/e9b962b5310010b4c2e7d677641b0518 /greetings-server \
&& pip install flask
CMD ["python3", "/greetings-server/greetings-api.py"]
Kurz gesagt basiert unser Image auf einem Python-Image, läd das Skript über Git herunter, installiert eine Abhängigkeit über pip und führt das Skript bei Container-Start aus. Gehen wir nun etwas genauer auf die Syntax ein:
Jedes Dockerfile beginnt mit einer FROM
-Anweisung. Sie gibt an, welches Image wir als Grundlage verwenden wollen. Für gängige Programmiersprachen existieren bereits offizielle Images (z.B. Python, Java oder Golang). Alternativ können Linux Distributionen wie Ubuntu oder Alpine als Basis verwendet werden. Für einige Anwendungsfälle kann es auch sinnvoll sein, mit dem leeren Image Scratch anzufangen.
Als nächste Instruktion finden wir in unserem Dockerfile den RUN
-Befehl, welcher Anweisungen in der Konsole ausführt. Da in unserem Basis-Image sowohl Git als auch Python vorinstalliert sind, können wir unser Testskript von Github herunterladen und das Python-Paket Flask über pip installieren.
Zuletzt legen wir in unserem Dockerfile fest, wie sich ein Container unseres Images standartmäßig verhalten soll, wenn er gestartet wird. CMD
übergeben wir dazu einen Befehl wie python3 zusammen mit Parametern wie der Pfad zu unserem Skript.
Ähnlich wird auch das Dockerfile für hello-world
aussehen, welches wir weiter oben getestet haben. Demgegenüber hatten wir beim Starten der ubuntu-Container den Startbefehl überschrieben, indem wir bash
bzw. ls
beim run
-Befehl angefügt haben.
Eine Anweisung, die wir nicht verwendet haben, aber durchaus nützlich ist, ist der COPY
-Befehl. Mit ihm können Dateien von unserem Dateisystem in das Image kopiert werden, z.B. hätten wir unser Skript auch so bereitstellen können:
COPY ./greetings-api.py /greetings-server/
Der Pfad in unserem Dateisystem ist relativ zu dem Pfad, den wir später als Kontext beim Bauen angeben. Dazu später mehr. Für eine vollständige Liste an Befehlen ist hier der Link zur offiziellen Dokumentation.
Unser Dockerfile ist fertig, also erzeugen wir damit ein Image:
▶ docker build -t greetings-api .
[+] Building 5.3s (6/6) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 43B 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/python:3.9 0.6s
=> CACHED [1/2] FROM docker.io/library/python:3.9@sha256:027e1a335355d82cc16b767fb7ba219f24787cfb13fc16d4e78cb8960cec320e 0.0s
=> [2/2] RUN git clone https://gist.github.com/JulianSauer/e9b962b5310010b4c2e7d677641b0518 /greetings-server && pip install flask 4.4s
=> exporting to image 0.2s
=> => exporting layers 0.2s
=> => writing image sha256:3e1015b3cf8de06cffa5975861ce550099ce5123f9041d92e11b0d99bb53950c 0.0s
=> => naming to docker.io/library/greetings-api 0.0s
Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
Wir haben dem build
-Befehl mit -t
einen Namen für unser Image sowie einen Pfad als Kontext übergeben. Dieser muss auf den Ordner verweisen, in dem unser Dockerfile liegt. Der Punkt verweist auf das aktuelle Verzeichnis, hier liegt unsere Datei ./Dockerfile
. Wenn du folgende Fehler siehst, hast du entweder keinen Pfad angegeben oder einen, in dem kein Dockerfile liegt:
▶ docker build -t greetings-api
"docker build" requires exactly 1 argument.
See 'docker build --help'.
Usage: docker build [OPTIONS] PATH | URL | -
Build an image from a Dockerfile
▶ docker build -t greetings-api ./falscher-ordner/
[+] Building 0.0s (1/2)
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 2B 0.0s
failed to solve with frontend dockerfile.v0: failed to read dockerfile: open /var/lib/docker/tmp/buildkit-mount017845978/Dockerfile: no such file or directory
Sofern alles geklappt hat, sollten wir unser Image lokal sehen können:
▶ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
greetings-api latest 3e1015b3cf8d 2 seconds ago 922MB
Als nächstes starten wir einen Container. Unser Skript nimmt Anfragen über den Port 5000 entgegen, dieser ist allerdings zunächst nur innerhalb des Containers erreichbar. Da wir auch von unserem Host-Betriebssystem darauf zugreifen wollen, müssen wir einen passenden Port an den Container weiterleiten. Aus Demonstrationszwecken habe ich den Port auf 5123 geändert. Typischerweise werden aber die gleichen Ports weitergeleitet. Beim run
-Befehl sehen wir also einerseits, dass der externe Port 5123 an den Port des Containers 5000 umgeleitet wird. Außerdem geben wir mit der Flag -d
an, dass der Container detached starten soll. Wir sehen also nicht wie zuvor die Konsolenausgabe, die unser Container erzeugt, sondern lediglich seine ID. Das hat den Vorteil, dass wir das Konsolenfenster schließen können, ohne den Container zu beenden.
▶ docker run -p 5123:5000 -d greetings-api
49b66edd5b43536d738e977259c7b26b7c3cbf0576f09ddb9b55273e179ad77b
Unser Rest-Service ist nun lokal unter dieser Adresse erreichbar:
Mit folgendem Befehl können wir den Zustand unseres Containers prüfen. Hier tauchen eventuell auch die Container aus vorherigen run
-Befehlen auf:
▶ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
49b66edd5b43 greetings-api "python3 /greetings-…" 8 seconds ago Up 7 seconds 0.0.0.0:5123->5000/tcp, :::5123->5000/tcp friendly_beaver
2f67b8d3a397 ubuntu "ls" 18 seconds ago Exited (0) 2 minutes ago agitated_mcclintock
8bcf963fc863 ubuntu "bash" 22 seconds ago Exited (0) 3 minutes ago mystifying_wu
1b018fd7614a hello-world "/hello" 41 seconds ago Exited (0) 4 minutes ago gracious_nobel
Um zum Schluss wieder aufzuräumen, stoppen wir zunächst den Container, der unseren Rest-Service enthält. Dazu verwenden wir entweder die Container ID oder den Namen:
▶ docker stop friendly_beaver
friendly_beaver
Anschließend können wir entweder mit docker rm <Container ID | Name>
einzelne Container entfernen oder mit docker container prune
automatisch alle beendeten Container löschen:
▶ docker container prune
WARNING! This will remove all stopped containers.
Are you sure you want to continue? [y/N] y
Deleted Containers:
49b66edd5b43536d738e977259c7b26b7c3cbf0576f09ddb9b55273e179ad77b
2f67b8d3a397267ab37b7f24d6361c6a09435f3d33265c485b04b4538e7e0b3f
8bcf963fc86393315af653d9b0f380cdfc44f9f5b79c48ecc885cb60b59e201d
1b018fd7614a954d071a8ab7818a11cb3fd9fd190230355ddec2ebe843d66281
Total reclaimed space: 119.4kB
Images können analog durch docker rmi <Image ID | Name>
gelöscht werden. Sofern lokal mehrere Versionen des gleichen Images exisitieren, muss beim Namen zusätzlich hinter einem Doppelpunkt die Version angegeben werden:
▶ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
python 3.8 79372a158581 10 days ago 909MB
python 3.9 e2d7fd224b9c 10 days ago 912MB
▶ docker rmi python:3.8 python:3.9
Untagged: python:3.8
Untagged: python@sha256:b39ab988ac2fa749273cf6fdeb89fb3635850824419dc612c8a21ff4c7961b8b
Deleted: sha256:79372a158581793fb231ecc5651ec224488f3340cc8db03db99c754783674208
Deleted: sha256:835052d5dabb0fd2f14b628bc144cc66e0b8b48c440cb973d8daa225ee495424
Deleted: sha256:8c17855d10e9b07dd5a91f1df96e27f33e91bba9914396a720d8d8b97f220e2e
Deleted: sha256:925707dc89ccc94fe096d174e6a95b40a53302f5f2ac32c5104d243f1fffd5e1
Untagged: python:3.9
Untagged: python@sha256:027e1a335355d82cc16b767fb7ba219f24787cfb13fc16d4e78cb8960cec320e
Deleted: sha256:e2d7fd224b9cd792b4dae4af48bcb91611a24cd580ae1db91fd0a56db1d8da47
Persistente Daten
Wird ein Container gelöscht, werden alle seine Daten mitgelöscht. Im Fall von zustandslosen Systemen oder einer kurzlebigen Test-Datenbank mag das vielleicht noch sinnvoll sein, aber spätestens bei der Produktivversion einer Datenbank sollte das nicht passieren. Deshalb können wir Ordner oder Dateien aus unserem Host-Dateisystem in den Container mounten. Dadurch geben wir dem Container Zugriff darauf und er arbeitet auf den gleichen Dateien und Ordnern. So werden beispielsweise die Daten unserer Datenbank auf unserem Dateisystem gespeichert und nicht nur innerhalb des Containers.
Anmerkung: In den Einstellungen von Docker muss eventuell der Zugriff auf das Dateisystem des Host-Betriebssystems erlaubt werden (Resources -> File Sharing
).
Als kleines Beispiel wird ein Ordner auf dem Host-Betriebssystem erstellt und in einen Container gemountet. Im Container wird eine Textdatei angelegt, welche anschließend auch außerhalb des Containers existiert. Mit der Flag -v <lokale Datei/Ordner>:<Zielpfad im Container>
können wir dazu die Verzeichnisse lokaler-ordner
(Host) und /home
(Container) miteinander verlinken. Da nur absolute Pfade angeben werden können, wird ${PWD} verwendet. Die Umgebungsvariable zeigt automatisch auf das Verzeichnis, in dem sich die Konsole aktuell befindet. Das klappt allerdings nicht bei allen Konsolen.
▶ mkdir lokaler-ordner # Ordner erstellen
▶ docker run -it -v ${PWD}/lokaler-ordner:/home ubuntu bash # Container starten
root@ad9ec07e44ae:/# echo "hi" >> /home/nachricht.txt # In neue Datei schreiben
root@ad9ec07e44ae:/# exit # Container verlassen
exit
▶ cat lokaler-ordner/nachricht.txt # Inhalt der neuen Datei ausgeben
hi
Architektur & Zusammenfassung
Mittlerweile können wir unsere Services in Containern laufen lassen, die entweder auf offiziellen oder selbst erstellten Images beruhen. Wir können Ports weiterleiten und unsere Daten persistent halten. Wenn wir uns wieder unser Anfangsbeispiel ansehen, können wir nun pro Komponente ein offizielles Image (Keycloak, PostgreSQL, Redis, Elasticsearch) oder ein selbst gebautes (Frontend, Backend) verwenden, um das gesamte System in Containern laufen zu lassen. Zu beachten ist, dass Images das Single-Responsibility-Prinzip verfolgen sollten. Wir installieren unsere Datenbank also nicht zusammen mit unserem Backend in einem Image, sondern trennen die beiden Komponenten voneinander. Dadurch sind sie leichter auszutauschen und voneinander entkoppelt. Wenn wir etwa unser Backend updaten, ist unsere Datenbank nicht davon betroffen.
Das vorerst letzte Problem ist, dass die docker run
-Befehle etwas ausarten können. Schließlich brauchen wir 6, da wir 6 Komponenten mit jeweils unterschiedlichen Images haben. Außerdem mag es Variationen geben, da Entwickler die Container mit anderen Konfigurationen auf ihren Laptops starten, als auf dem Test-/Produktivsystem. Es kann deshalb sinnvoll sein, docker-compose zu verwenden, um darüber die verschiedenen Konfigurationen schriftlich in einer docker-compose.yml
-Datei festhalten und starten zu können. Das würde an dieser Stelle allerdings zu weit führen. Wir geben uns deshalb damit zufrieden, dass wir unsere Entwicklung effizienter gestalten konnten, da unsere Architektur übersichtlicher geworden ist und deren Betrieb einfacher. Das Teilen von Schwimmnudel-Bildern war nie einfacher!
Die wichtigsten Befehle
docker run <image>
: Startet einen neuen Container von einem Imagedocker build -t <image> .
: Erzeugt ein neues Image anhand eines Dockerfiles (s. nächster Abschnitt)docker images
: Listet alle lokalen Images aufdocker rmi <image>
: Löscht ein lokales Imagedocker ps -a
: Listet alle laufenden und beendeten Containerdocker stop <container>
: Stoppt einen Containerdocker rm <container>
: Löscht einen gestoppten Container
Glossar
- PostgreSQL: Eine relationale Datenbank
- Keycloak: Open Source Lösung für Benutzer- und Rechteverwaltung
- Redis: Key-Value Datenbank, die aufgrund ihrer Performanz gerne als Cache verwendet wird
- ElasticSearch: Suchmaschine an die Text geschickt werden kann, um ihn später durchsuchen zu können
- Git: Versionsverwaltungssystem mit dem Dateien in Repositories in verschiedenen Versionen gespeichert und mit Online-Diensten/anderen Nutzern synchronisiert werden können
- Github: Online-Dienst, über den Git Repository synchronisiert werden können
- Hyper-V: Virtualisierungssystem von Windows – Docker läuft nativ nur unter Linux
- WLS 2 (Windows Subsystem for Linux): Erlaubt das Ausführen von Linux-Programmen unter Windows
- Bash (Bourne-again shell): Eine Kommandozeile unter Unix
- Rest-API (Representational State Transfer-Application Programming Interface): Eine Schnittstelle, an die man über HTTP Daten senden und empfangen kann; da die meisten internetfähigen Geräte HTTP unterstützen, ist eine solche Schnittstelle quasi universell ansprechbar
- Python: Eine interpretierte Programmiersprache
- pip: Paketmanager von Python; über ihn können Bibliotheken heruntergeladen werden, um sie in Python zu verwenden
- Flask: Ein Webframework in Python; es wird hier benutzt, um eine einfache Rest-Api zu erstellen