Als Grundlage für mein Homelab habe ich ein Proxmox Cluster installiert, der Hochverfügbar eingerichtet ist. Dazu sind drei Cluster-Nodes installiert, die einen gemeinsamen Speicher über Ceph für die Virtuellen Maschinen (VMs) haben. Damit können VMs im Falle eines Neustarts zwischen den Proxmox Nodes ohne Verzögerung hin und her wandern.
Da ich Docker Container nutzen möchte, benötige ich zumindest eine VM. Wird diese VM jedoch im Rahmen von regelmäßigen Updates neugestartet, sind alle Docker Container im Rahmen dieses Updates nicht erreichbar. Ein Docker Swarm Cluster kann hierfür eine Lösung sein. Weiterhin ist das Loadbalancing auf die drei physikalischen Knoten mit nur einer Docker VM nicht möglich. Auch dafür kann der Docker Swarm Cluster eine Lösung darstellen.
Ich schreibe diese Anleitung in erster Linie für mich als „Installationsdokumentation“, damit ich bei eventuellen späteren Neuinstallationen eine Gedächtnisstütze habe. Es kann deswegen gut sein, dass manche Teile durch Updates in Zukunft nicht mehr korrekt sind.
Meine Quellen waren für diese Installation im Wesentlichen:
- https://www.och-group.de/2024/11/proxmox-cluster-mit-ceph-auf-minisforum-ms-01/
- https://www.openmediavault.org
- https://docs.docker.com/engine/install/debian/
- https://www.virtualizationhowto.com/2024/10/docker-swarm-is-awesome-with-portainer/
- https://gist.github.com/abasu0713/cb522b6ba4489ac15838465fc87faf17
- https://www.virtualizationhowto.com/2024/10/cephfs-for-docker-container-storage/
- https://www.youtube.com/watch?v=S2VuHKxrT3s
- https://www.docker.com/blog/how-to-fix-and-debug-docker-containers-like-a-superhero/
- https://snapcraft.io/microceph
- https://docs.portainer.io/admin/environments/add/swarm/agent
- https://github.com/digikin/swarm-tick-stack-demo/blob/master/portainer-agent-stack.yml#L24
Openmediavault Installation
Openmediavault basiert auf Debian, hat aber zusätzlich noch eine schöne Weboberfläche, über die die Administration vereinfacht wird. Insbesondere wenn bestehende persistente Docker Verzeichnisse verschoben werden sollen, ist es einfach, einen SMB/CIFS Share zu erstellen und dort einfach alles drauf zu kopieren. Zusätzlich ist es einfacher neue Festplatten einzuhängen oder bestehende zu vergrößern.
Für die VM habe ich folgende Einstellungen gewählt:
- 4 Cores, 1 Socket
- 16G RAM
- OS-Festplatte: 30G
- Zusätzliche Festplatte für microceph: 100G
- Eine Netzwerkkarte
Die Installation geht recht einfach vonstatten, nachdem die VM mit der Openmediavault ISO gebootet ist:
- Sprachen-Einstellungen: Deutsch, Deutschland, Deutsch
- Rechnername: docker-swarm-01 (und natürlich 02 und 03 für die anderen zwei Nodes)
- Domainnamen vergeben
- Root Passwort vergeben
- Spiegelserver in der Nähe auswählen
- Gerät für Bootloader-Installation auswählen
- Reboot & CD auswerfen
Jetzt kann man sich im Webinterface auf dem Server anmelden mit den Default Login-Daten. User: admin – Password: openmediavault. Das wird als erstes geändert im Menü. Rechts oben auf das User-Icon klicken und „Passwort ändern“ auswählen. Neues Passwort eintippen und fertig.
Updates werden einfach übers Webinterface gestartet:
System -> Aktualisierungsverwaltung -> Aktualisierungen -> Pfeil runter mit der „Nummer“ klicken -> Bestätigen. Warten. Konfigurationsänderungen bestätigen. Reboot.
Jetzt noch eine statische IP vergeben für das System:
Netzwerk -> Schnittstellen -> Interface bearbeiten:
- IPv4
- Methode: Statisch
- Adresse: Freie Adresse im Heimnetzwerk, beispielsweise 10.0.1.11 und für die anderen zwei Nodes hinten die 12 und die 13
- Netzmaske: 255.255.255.0
- Gateway: Sollte bekannt sein, üblicherweise der Router. Beispielsweise: 10.0.1.1
- DNS Server: Sollte bekannt sein, üblicherweise ebenfalls der Router. Beispielsweise 10.0.1.1
- Ausstehende Konfigurationsänderungen anwenden
- Es scheint einzufrieren, einfach die neu konfigurierte IP im Browser aufrufen. Die IP wurde schon umgestellt.
Installation von Docker und Docker Compose
Zuerst installiere ich kleinere Helferlein:
apt install qemu-guest-agent htop snap snapd -y
Nun geht es nach der offiziellen Docker Dokumentation weiter:
# Add Docker's official GPG key:
apt update
apt install ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update
Den Erfolg des Befehls kann man sich mit dem nachfolgenden Befehl anschauen. Hier sollte der Eintrag zum Repository von Docker zu sehen sein:
nano /etc/apt/sources.list.d/docker.list
Dann wird Docker installiert:
apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Den Erfolg der Installation von Docker kann man auch wieder überprüfen. Hierzu wird einfach ein „hello-world“ image heruntergeladen und gestartet. Als Ergebnis gibt es eine Bestätigungsmeldung und der Container beendet sich wieder.
docker run hello-world
Wiederholen für Node 2 und Node 3
Den bisherigen Teil der Anleitung für die zwei weiteren Nodes durchführen, damit alle auf dem gleichen Zustand sind. Danach kann man mit Swarm loslegen.
Docker Swarm installation
Auf Node 1 starten wir die Initialisierung vom Swarm cluster mit folgendem Befehl:
docker swarm init --advertise-addr 10.0.1.10
Als Ergebnis wird der Join-Command angezeigt mit dem dafür notwendigen Token. Den Join-Command kopiert man und tippt ihn in die Node 2 und 3 ein.
docker swarm join --token <<<YOURTOKEN>>> 10.0.1.11:2377
Damit werden die zwei hinzugefügten Nodes „worker“ im Swarm. Das wird nun dem Node 1 bekannt gemacht, indem man im Node 1 eintippt:
docker node promote docker-swarm-02
docker node promote docker-swarm-03
docker node ls
Der letzte Befel listet alle aktuellen Hosts auf.
Virtuelle IP-Adressen
Damit die Docker Container nach außen mit einer IP Adresse sprechen können und zwar unabhängig vom jeweiligen Docker Host auf dem sie laufen, wird nun eine Virtuelle IP Adresse erstellt. Dazu gibt es da Paket keepalived, das installiert wird:
apt install keepalived -y
Auf Node 1 wird keepalived konfiguriert:
nano /etc/keepalived/keepalived.conf
Und folgender Inhalt eingefügt (Achtung, der Interface Name kann abweichen!). Dabei werden unter „unicast_peer“ die IP-Adressen der anderen Hosts eingetragen, unter „virtual_ipadress“ die zusätzliche virtuelle IP. Das auth_pass ändern:
vrrp_instance VI_1 {
state MASTER
interface ens192
virtual_router_id 51
priority 120
advert_int 1
authentication {
auth_type PASS
auth_pass 123456789Password
}
unicast_peer {
10.0.1.12
10.0.1.13
}
virtual_ipaddress {
10.0.1.15
}
}
Für die Nodes 2 und 3 wird ebenfalls die Konfigurationsdatei erstellt:
nano /etc/keepalived/keepalived.conf
Und ein ähnlicher Inhalt eingetragen. Geädert ist „state“ – hier nur als BACKUP. Die Priority muss niedriger sein als die von Node 1. Die unicast peers sind jeweils die zwei anderen Nodes, also nicht die Node, auf der gerade die Datei angelegt wird. Bswp. für Node 2:
vrrp_instance VI_1 {
state BACKUP
interface ens192
virtual_router_id 51
priority 110
advert_int 1
authentication {
auth_type PASS
auth_pass 123456789Password
}
unicast_peer {
10.0.1.11
10.0.1.13
}
virtual_ipaddress {
10.0.1.15
}
}
Und für Node 3 beispielsweise (Priority nochmals niedriger als bei Node 2):
vrrp_instance VI_1 {
state BACKUP
interface ens192
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass 123456789Password
}
unicast_peer {
10.0.1.11
10.0.1.12
}
virtual_ipaddress {
10.0.1.15
}
}
Dann wird der Service keepalived auf allen drei nodes gestartet und für Neustarts aktiviert:
systemctl start keepalived
systemctl enable keepalived
systemctl status keepalived
Jetzt kann man die neue IP Adresse pingen und erhält eine Antwort.
ping 10.0.1.15
Gemeinsamer Speicher mit Ceph
Gemeinsamer Speicher kann hier recht einfach über Ceph – genauer microceph realisiert werden. Microceph ist deutlich einfacher zu installieren als Ceph selbst. Es wird auf allen drei Nodes installiert:
export PATH=$PATH:/snap/bin
snap install microceph
snap refresh --hold microceph
Mit dem hold Befehl wird die aktuelle Version gepinnt, sodass Updates nur dann ausgeführt werden, wenn das aktiv angestoßen wird. Auf Node 1 wird Ceph initialisiert und die Nodes 2 und 3 hinzugefügt:
microceph cluster bootstrap
microceph status
microceph cluster add docker-swarm-02
microceph cluster add docker-swarm-03
Auf Node 2 und Node 3 verwendet man die über den add erzeugten Token, um dem Cluster beizutreten:
microceph cluster join <<<YOURTOKEN>>>
Jetzt können Festplatten zum Cluster hinzugefügt werden. Über lsblk sieht man die noch nicht verwedendete Festplatte. Bei mir lautet der Name: /dev/sdb. Aber dieser Name kann je nach Node abweichend sein! Den folgenden Befehl (angepasst) auf allen drei Nodes ausführen.
microceph status
microceph disk add /dev/sdb --wipe
Als Ergebnis wird jeweils success angezeigt. Auf Node 1 geht es dann mit den eingebundenen Festplatten weiter:
microceph status
ceph osd pool create cephfs_data 64
ceph osd pool create cephfs_metadata 64
ceph fs new cephfs cephfs_metadata cephfs_data
ceph fs ls
Jetzt wird auf jedem Node ein Verzeichnis angelegt, in das das Ceph Dateisystem hineingemountet werden kann.
mkdir /mnt/cephfs
Als nächstes wird ein cefh admin token benötigt, mit dem der Mount durchgeführt werden kann. Dafür auf Node 1 ausführen:
ceph auth get-key client.admin
Mit diesem Token wird die fstab für alle drei Nodes angepasst:
nano /etc/fstab
und folgende Zeile am Ende eingefügt. Dabei wird hinter „secret=“ das Token eingesetzt und die IP-Adressen vorne entsprechen allen IPs der Nodes:
10.0.1.11:6789,10.0.1.12:6789,10.0.1.13:6789:/ /mnt/cephfs ceph name=admin,secret=<<<TOKEN>>>,_netdev 0 0
Mit “ mount -a “ wird das neu Laden der fstab erzwungen. Danach wird der Deamon neu geladen. Per df wird dann das neue Dateisystem angezeigt.
mount -a
systemctl daemon-reload
df -h
Damit ist in /mnt/cephfs ein persistenter Speicher hinterlegt.
Portainer installation
Bei Portainer gibt es die Möglichkeit für drei Nodes eine kostenlose Lizenz für die Enterprise Edition zu holen. Dazu einfach kurz googlen, anmelden und recht zügig erhält man die Lizenzschlüssel per Mail.
mkdir /mnt/cephfs/containers/container-portainer/data
cd /mnt/cephfs/containers/container-portainer
nano compose.yaml
Mit folgendem Inhalt:
version: '3.2'
services:
agent:
image: portainer/agent:latest
environment:
# REQUIRED: Should be equal to the service name prefixed by "tasks." when
# deployed inside an overlay network
AGENT_CLUSTER_ADDR: tasks.agent
# AGENT_PORT: 9001
# LOG_LEVEL: debug
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/lib/docker/volumes:/var/lib/docker/volumes
networks:
- agent_network
deploy:
mode: global
placement:
constraints: [node.platform.os == linux]
portainer:
image: portainer/portainer:latest
command: -H tcp://tasks.agent:9001 --tlsskipverify
ports:
#- "9000:9000"
- "9443:9443"
volumes:
- /mnt/cephfs/containers/portainer/data:/data
networks:
- agent_network
deploy:
mode: replicated
replicas: 1
placement:
constraints: [node.role == manager]
networks:
agent_network:
driver: overlay
attachable: true
Mit den folgenden Befehlen startet Portainer hoch und ist per https auf dem Port 9443 zu erreichen.
docker node ls
docker stack deploy --compose-file compose.yaml portainer --detach=false
docker service ls
docker stack services portainer
Beim ersten Start erstellt man für Portainer den Benutzer admin und vergibt selbst ein sicheres Passwort.
Portainer zu Enterprise Edition migrieren
Für den privaten Gebrauch kann man die „free for three nodes“-Lizenz verwenden. Dazu wird der Container einfach aktualisiert auf einem Node:
docker service update --image portainer/portainer-ee:latest --force portainer_portainer --detach=false
Nach dem Neustart des Portainer Containers muss im Webinterface der per Mail erhaltene Code eingegeben werden. Dazu muss man sich erneut anmelden. Danach alles neu starten:
docker service update --force portainer_portainer
docker service update --force portainer_agent
Einloggen und die Environment ist auch wieder da. Oben links steht nun auch „Business Edition“. Die Änderungen sollte man auch in der yaml nachziehen. Ob alles funktioniert, kann man prüfen, indem man den Stack herunterfährt und neu startet:
cd /mnt/cephfs/containers/container-portainer
docker stack deploy portainer --compose-file docker.yaml --detach=false
Quelle: https://docs.portainer.io/start/upgrade/tobe/swarm
Wer sich über die Services wundert, die in der Swarm übersicht immer dazukommen, wenn ein Service migiert wird:
Kurz: Das ist normal. Man kann aufräumen mit folgendem Befehl auf einem Manager Node:
docker swarm update --task-history-limit=1
Quelle: https://stackoverflow.com/questions/42364695/how-to-clear-docker-task-history
Aufräumen und Speicherplatz freigeben
docker system prune --all
Das gibt dann folgende Meldung, die mit „y“ zu bestätigen ist:
WARNING! This will remove:
- all stopped containers
- all networks not used by at least one container
- all images without at least one container associated to them
- all build cache
Are you sure you want to continue? [y/N]
Cluster Node in den Wartungsmodus schicken und wieder aktivieren
Will man einen Node mit Updates versorgen, kann man einzelne Nodes deaktivieren, die Updates durchführen und danach den Node wieder aktiv schalten. Den Node deaktivieren und damit auch alle Services, die auf dem Node laufen:
docker node update --availability drain docker-swarm-03
Prüfen, ob der Befehl erfolgreich war:
docker node ls
Den Node wieder aktiv schalten geht mit:
docker node update --availability active docker-swarm-03
Quellen:
https://forums.docker.com/t/graceful-restart-of-swarm-manager-leader/114510
https://docs.docker.com/engine/swarm/swarm-tutorial/drain-node/
YAML als Stack deployen
Da nun alle Services auf Docker Swarm laufen, muss definiert werden, wie viele Instanzen (in Docker Swarm sind das „tasks“) eines jeden Services (auch innerhalb eines Stacks) gleichzeitig laufen dürfen.
Als Faustregel gilt: Es sind nur die Services skalierbar in der Anzahl (Parallelität), die stateless sind. Damit ergibt sich folgende einfache Ergänzung der yaml Dateien:
# Docker Swarm Single
deploy:
mode: replicated
replicas: 1
placement:
constraints: [node.role == manager]
Bei einem Update kann unter „Options“ der Eintrag „Prune Services“ aktiviert werden. Damit wird der Stack aktualisiert und alte Überbleibsel (bspw. von einem früheren migrate) werden entfernt.
Rolling Updates für einen Stack
Möchte man das rolling update ausprobieren geht das grundsätzlich ganz einfach.
Ein Hinweis dabei: Es funktioniert nicht bei Services, die den „mode: global“ im deployment haben, sonsdern nur bei „replicated“ services.
Ich habe mir für den Anwendungsfall einen watchtower Container aufgesetzt – einfach als neuen Stack mit folgender Konfiguration:
version: "3"
services:
watchtower:
restart: unless-stopped
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_DEBUG=true
- WATCHTOWER_INCLUDE_RESTARTING=true
# Entry | Description | Equivalent To
# ----- | ----------- | -------------
# @yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 *
# @monthly | Run once a month, midnight, first of month | 0 0 0 1 * *
# @weekly | Run once a week, midnight between Sat/Sun | 0 0 0 * * 0
# @daily (or @midnight) | Run once a day, midnight | 0 0 0 * * *
# @hourly | Run once an hour, beginning of hour | 0 0 * * * *
# I want watchtower to check for new updates every night at 4am.
- WATCHTOWER_SCHEDULE=0 */10 * * * *
- TZ=Europe/Berlin
# Docker Swarm 3 replicas
deploy:
mode: replicated
replicas: 3
placement:
constraints: [node.role == manager]
update_config:
parallelism: 1
delay: 120s
Es ist also ein Container, der drei mal parallel läuft, aber im Falle des Updates nur eine Instanz parallel aktualisiert. Möchte man den Updatevorgang beobachten geht das unter „Services“ im portainer – da muss man jedoch die Seite immer wieder neu laden. Einfacher geht das Beobachten per Konsole:
docker service update --force watchtower_watchtower
Ein task fährt sauber herunter und nach einer Wartezeit startet der neue task erst. Damit ist immer nur ein task nicht zu erreichen / nicht am arbeiten. Die verbleibenden tasks erledigen weiter die Arbeit. Damit ergbit sich für mich folgende Konfiguration, die ich einfach bei jedem Service einfüge, damit dieser Service auf dem Swarm cluster läuft und ich damit die automatische Failover-Funktion verwenden kann.
# Docker Swarm Single
deploy:
mode: replicated
replicas: 1
placement:
constraints: [node.role == manager]
update_config:
parallelism: 1
delay: 120s
Anmerkung: Der automatische Failover ist aktiv – der Docker Service läuft also auch dann noch, wenn ein Server zwecks Updates neustartet. Und dennoch gibt es bei einer Migration von einem Swarm Host zum anderen eine Unterbrechung. Denn der Failover dauert in Summe schlicht zu lang. Zusätzlich wird man ausgeloggt.
Der Vorteil dieser Art der Installation hält sich also in Grenzen. Für aufwändigere Updates oder für längeres Debugging hilft dieses Setup.
Quellen:
https://docs.docker.com/engine/swarm/swarm-tutorial/deploy-service/
https://docs.docker.com/reference/compose-file/deploy/#update_config
https://www.youtube.com/live/dLBGoaMz7dQ
https://www.geeksforgeeks.org/docker-swarm-rolling-updates-rollbacks/
Ordner für volumes werden nicht erstellt
Hier scheint es einen signifikanten Unterschied zum standalone Docker zu geben. Beim Swarm mode müssen wohl die Ordner und Dateien (volumes) auf dem gemeinsamen Speicher manuell angelegt werden.
Quellen:
https://www.reddit.com/r/docker/comments/1768qe7/docker_named_volumes_not_creating_folders_on_host/
Nach meinem Verständnis die logischer Konsequenz: Docker Swarm kennt den darunterliegenden replizierenden Speicher nicht. Wird also ein Container erstellt, ist unklar auf welchem Node das zu erfolgen hat, damit Arbeits- und Speicherressource zusammen funktionieren.