Grundsätzlich lässt sich Docker recht einfach installieren, da die Pakete in allen gängigen Linux-Systemen bereits vorhanden sind.
Doch hier möchte ich meinen persönlichen Ansatz für die Verwaltung und Strukturierung noch ein Wenig erklären, wie man sich vielleicht ein bisschen leichter tut.
Die Installation von Docker erfolgt denkbar einfach:
apt-get install docker.io docker-compose
Sofern die Installation der aktuellsten Version gewünscht wird, so kann man die aktuellere Quelle über das autoamtische Skript hinzufügen lassen:
curl -sSL https://get.docker.com > install-docker.sh
bash ./install-docker.sh
curl -sSL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) > /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
Ich habe für alle Docker-Container eine hierarchische Ordnerstruktur, die in erster Instanz versucht den Zweck zu definieren. Basis-Verzeichnis ist dabei /opt/docker
.
Anschließend unterteile ich gerne in
services
: Das sind wirklich nur reine Sevices, gemeint, wie z.B. ein MQTT Brokerhosting
: Hiermit sind alle Webseiten gemeint (also alles, was mit dem Browser angesehen werden kann)databases
: Sofern die Anwendung keinen eigenen Datenbank-Container spendiert bekommt, so liegen hier quasi "die Installationen", die anwendungsübergreifend benutzt werden. Bei diesen Datenbanken sind auch die Ports in das lokale System übertragen.monitoring
: Alle Anwendungen, die ich für mein Monitoring benutze, z.B. Prometheus, Grafana, etc./
|-- opt/
| |-- docker/
| | |-- databases/
| | | |-- mysql/
| | | |-- postgres/
| | | `-- ...
| | |-- hosting/
| | | |-- nextcloud/
| | | |-- nuget/
| | | |-- wiki/
| | | `-- ...
| | |-- services/
| | | |-- mqtt/
| | | |-- teamspeak/
| | | `-- ...
| | `-- ...
| `-- ...
`-- ...
In der Konfiguration von Docker nach der Installation ist erst einmal nur IPv4 möglich.
Doch es gibt zwei Wege, wie man dennoch IPv6 ermöglichen kann.
Mittlerweile ist Docker schon etwas weiter mit seiner Unterstützung von IPv6, weshalb diese Variane mittlerweile bevorzugt werden kann.
Dazu erweitert (oder erzeugt) man eine Datei /etc/docker/daemon.json
:
{
"iptables": true,
"fixed-cidr": "172.17.0.0/16",
"ipv6": true,
"ip6tables": true,
"fixed-cidr-v6": "fd00:172:17::/64",
"experimental": true
}
Anschließend muss der Docker-Daemon einmal neu gestartet werden:
systemctl restart docker
Eine weitere, recht einfache Methode für IPv6 aus einem Docker Container heraus, stellt das IPv6 NAT dar (selbst ein Container ):
/opt/docker/monitoring/ipv6nat/docker-compose.yml
:
version: '3'
services:
ipv6nat:
image: robbertkl/ipv6nat
container_name: ipv6nat
restart: always
network_mode: host
privileged: true
environment:
TZ: Europe/Berlin
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /lib/modules:/lib/modules:ro
Anschließend können wir den Container starten:
docker-compose up -d
Der Container prüft über den Zugriff auf den Docker-Socket, welche Container existieren und mit einer IPv6 Adresse versorgt sind. Anschließend übernimmt der Container die Aufgabe alle IPv6 Anfragen zu vermitteln.
Ich habe mir angewöhnt einzelne Docker-Container, die kein eigenes Subnet benötigen (Single-Service-Container) in ein eigenes Netz zu hängen, das speziell konfiguriert ist.
Ich nenne dieses Netz isolated
, denn die Container können nicht untereinander kommunizieren.
Legen wir also dieses Netzwerk an, sodass Container sich damit verbinden können:
docker network create -d bridge \
--subnet=10.20.0.0/16 \
--gateway=10.20.0.1 \
--subnet=fd0a::10:20:0:0/112 \
--gateway=fd0a::10:20:0:1 \
-o com.docker.network.bridge.enable_icc=false \
-o com.docker.network.bridge.enable_ip_masquerade=true \
-o com.docker.network.bridge.host_binding_ipv4=127.0.0.1 \
-o com.docker.network.bridge.host_binding_ipv6=::1 \
-o com.docker.network.bridge.name=isolated \
-o com.docker.network.driver.mtu=1500 \
isolated
Hier die Bedeutung der wichtigen Parameter
subnet
definiert das IP Netzwerk (sowohl IPv4 als auch IPv6)gateway
definiert die IP Adresse des Hostsystemsd
ist die Kurzschreibweise für driver
und ist eine Bridge (Daten können weitergeleitet werden)o
ist die Kurzschreibweise für opt
... die erweiterten Optionen
enable_icc
ICC steht für Inter-Container-Communication, also ob die Container untereinander kommuniziern dürfenenable_ip_masquerade
sorgt dafür, dass die Paketweiterleitung funktionierthost_binding_ipv4
definiert an welche Adresse auf dem HOST-System gebunden werden soll (hier: localhost)host_binding_ipv6
das gleich noch einmal für IPv6... auch localhostDie Verwendung von Compose-Dateien hat den charmanten Vorteil, dass mehrere Container gemeinsam gestartet und gestoppt werden können. Dies ermöglicht es z.B. einer WebAnwendung seine eigene Datenbank mit zu starten.
Wichtige Eckpunkte, die es für docker-compose
zu beachten gilt:
docker-compose
sucht im aktuellen Verzeichnis nach der gleichnamigen yml-Datei, alle anderen Dateinamen/Pfade müssen mittels -f
angegeben werdenEin Beispiel kann oben unter IPv6 eingesehen werden.
Wenn gewünscht, können die einzelnen Container mittels Prometheus und Grafana überwacht werden. Hierfür stellt Google einen Container bereit.
/opt/docker/monitoring/container/docker-compose.yml
:
version: '3'
services:
cadvisor:
image: google/cadvisor
container_name: cadvisor
restart: unless-stopped
networks:
- isolated
ports:
- 8080:8080
environment:
TZ: Europe/Berlin
volumes:
- /:/rootfs:ro
- /var/run:/var/run:rw
- /sys:/sys:ro
- /var/lib/docker:/var/lib/docker:ro
labels:
com.centurylinklabs.watchtower.enable: true
networks:
isolated:
external: true
Ebenso können Images, sowie die Container, die auf den jeweiligen Images basieren, automatisch aktualisiert werden. Einen Anhaltspunkt dafür ließ sich schon in den bisherigen Compose Dateien finden.
Das Tool, bzw. das Image, heißt Watchtower.
version: '3'
services:
watchtower:
image: containrrr/watchtower
container_name: watchtower
restart: unless-stopped
command: --cleanup --label-enable --schedule "0 30 5 * * 1"
networks:
- isolated
environment:
TZ: Europe/Berlin
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
isolated:
external: true
Sofern Docker an einer Registry angemeldet ist, so liegen die Anmeldedaten im Benutzerordner versteckt: /root/.docker/config.json:/config.json:ro
.
Hier im Beispiel sucht Watchtower je Woche montags morgens um 05:30 Uhr nach neuen Aktualisierungen für Container, die das Label (com.centurylinklabs.watchtower.enable: true
) tragen. Ebenso werden im Anschluss alte Images automatisch gelöscht, um Speicherplatz zu sparen.
Es besteht die Möglichkeit, dass man Docker über eine Weboberfläche steuert. Dies ist die Portainer-Oberfläche. Im Hintergrund gibt es dafür zwei Images. Einmal die Anwendung selbst, die die Weboberfläche zur Verfügung stellt. Anschließend wird noch der Portainer-Agent benötigt, der die Schnittstelle zu Docker selbst herstellt.
So ist es möglich mehrere Server damit zu verwalten.
/opt/docker/monitoring/portainer/docker-compose.yml
:
version: '3'
services:
server:
image: portainer/portainer-ce:alpine
container_name: portainer-server
restart: unless-stopped
networks:
net:
aliases:
- server
ports:
- 127.0.0.1:9000:9000
environment:
TZ: Europe/Berlin
AGENT_SECRET: 'supersecuretoken'
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /opt/docker/monitoring/portainer/volumes:/data
labels:
com.centurylinklabs.watchtower.enable: true
agent:
image: portainer/agent
container_name: portainer-agent
restart: unless-stopped
networks:
net:
aliases:
- agent
environment:
TZ: Europe/Berlin
AGENT_SECRET: 'supersecuretoken'
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/lib/docker/volumes:/var/lib/docker/volumes
- /:/host
labels:
com.centurylinklabs.watchtower.enable: true
networks:
net:
driver: bridge
driver_opts:
com.docker.network.bridge.name: portainer-net
ipam:
driver: default
config:
- subnet: 10.21.0.0/24
Nun kann man den Webserver als Reverse Proxy verwenden und die Portainer-Anwendung nach außen leiten. Intern lauscht der Container auf Port 9000 gebunden auf den localhost. Der eigene lokale Agent kann dabei direkt intern innerhalb des Docker-Netzes über den DNS-Namen agent angesprochen werden.
buildx
Wenn Images für mehrere Platformen bauen möchte, kann man dies mit dem Docker-Plugin buildx
machen. Weitere Informationen (sowie das aktuellste Release) zu buildx
findet ihr auf Github.
Dieser Teil der Anleitung nutzt ein Debian 12 (bookworm) amd64 System.
Nach der Installation von Docker selbst, legt man dieses Plugin im dafür vorgesehenen Ordner ab.
Die Orte können auf der Github Seite eingesehen werden. Hier verwenden wir /usr/local/lib/docker/cli-plugins
.
mkdir -p /usr/local/lib/docker/cli-plugins
curl -sSL https://github.com/docker/buildx/releases/download/v0.12.1/buildx-v0.12.1.linux-amd64 > /usr/local/lib/docker/cli-plugins/docker-buildx
chmod +x /usr/local/lib/docker/cli-plugins/docker-buildx
Hier wird die Verwendung von buildx
mittels Emulation (qemu) beschrieben.
Zur Erstellung (und Nutzung) eines eigenen Builders verwenden wir folgenden Befehl:
docker buildx create --use --bootstrap --driver docker-container --name <builder-name>
Hinterher löschen können wir ihn wieder mit:
docker buildx stop <builder-name>
docker buildx rm <builder-name>
Einen Cross-Compile ausführen können wir recht einfach mittels:
docker buildx build --output "type=image,push=true" \
--builder <builder-name> \
--platform "linux/amd64,linux/arm64,linux/arm/v7" \
--tag registry/image:latest \
--tag registry/image:x.y.z \
.
Wie man schön sieht, können mehrere Tags definiert werden (spezifische Version und "latest").
Ebenso sehen wir unter --platform
für welche Zielplatformen gebaut wird (hier: linux/amd64
, linux/arm64
und linux/arm/v7
- das ist der Raspberry Pi mit 32bit System).
Wichtig ist auch der letzte Punkt unten, denn dies ist das Build-Verzeichnis (aktuelles Verzeichnis).
Dockerfile
Beim Cross-Compile wir das Dockerfile am Ende pro Platform einmal aufgerufen.
Sofern es sich bei der Zielplatform nicht um die eigene handelt, wird per qemu das andere System emuliert - was natürlich Performance kostet.
Sollte also die Programmiersprache dazu in der Lage sein, nativ cross-compiling zu betreiben, sollte man dies nutzen!
Es gibt ein paar Umgebungsvariablen, die durch Docker gesetzt werden.
Schlüsseln wir mal auf... wir nutzen dafür einen alten Raspberry Pi...
linux/arm/v7
linux
- also der erste Teil der gesamten Platformarm
- also der zweite Teil der Platform-Angabev7
- also der dritte TeilWir können festhalten, dass eine Platform immer OS und ARCH angeben... und manchmal dann speziell noch die VARIANT (linux/amd64
hat z.B. keine Variante).
Um diese Umgebungsvariablen in useren Dockerfiles nutzen zu können, müssen wir sie innerhalb der Image-Definitionen dann "durchschleifen". Dies geschieht z.B. mittels ARG TARGETARCH
.
Schauen wir uns einfach mal ein Dockerfile
an, mit dem eine .NET Anwendung gebaut wird.
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 as build
ARG TARGETARCH
RUN git clone https://github.com/some/dotnet-project.git && \
cd dotnet-project && \
git reset --hard x.y.z && \
dotnet publish -c Release -a $TARGETARCH --self-contained -nologo
FROM busybox
COPY --from=build /dotnet-project/bin/Release/*/publish/ /app
RUN chmod +x /app/dotnet-project
WORKDIR /app
CMD ["/app/dotnet-project"]
Es wird mit zwei Images gearbeitet. Das erste zum Bauen der Anwendung. Dies geschieht jedes Mal in einem nativen Image mit der Architektur des Hosts - es wird also keine Emulation benötigt und ist entsprechend performant.
Anschließend wird das zweite Image in der Zielplatform geholt (keine Angabe von --platform
im FROM bedeutet immer in der Zielplatform), der fertige Build aus dem ersten Image geholt und sauber im zweiten (ziel) Image platziert.
Dadurch ist der Abdruck des finalen Images kleiner und hat dennoch alles, was benötigt wird.