🐳 Provisionieren eines Docker-Hosts

Lange wurde hier im Blog nichts mehr geschrieben, aber nun möchte ich das Schreiben wieder aufnehmen. Bitte entschuldigt, wenn der Satzbau noch etwas holprig ist. Das wird sich mit den kommenden Artikel alles wieder finden.

Ich möchte heute ein paar Worte über die Einrichtung eines Servers mit Docker, also einem Docker-Host verlieren.

Für den schnellen Going-Live einer Applikation bietet sich noch immer ein Root-Server, egal ob Bare Metal oder VM, an. VMs gibt es mittlerweile wie Sand am Meer und man ist noch nicht mal gezwungen zu einem der drei Großen (AWS, Azure, Google Cloud) zu gehen. Empfehlen kann ich beispielsweise die VMs in der Hetzner Cloud.

Ich gehe in diesem Beispiel von einer Maschine mit Debian Buster aus, zu der ihr Root-Zugang erhalten haben solltet. Debian ist bei mir das Mittel der Wahl, weil es im Verhältnis zu einigen anderen Distributionen, wie beispielsweise Ubuntu noch mal deutlich schlanker daherkommt. Weniger Pakete reduzieren den Update-Aufwand und die möglichen Angriffsvektoren. Das Host-System muss ja auch eigentlich gar nicht viel können. Im Gegenteil: Ich möchte es sauber halten, um mich vom Wirt nicht zu stark abhängig zu machen und ihn im Zweifel auch schnell wieder ersetzen zu können - parasite mode on!

Docker Engine - der Stoff aus dem die Träume gemacht sind 🐳

Die offizielle Dokumentation beschreibt ziemlich gut, wie man Docker auf der Maschine installiert bekommt. Ein paar Gedanken dazu:

  1. Ich nutze grundsätzlich die Installation mittels Repository, worauf in diesem Artikel auch der Fokus liegt.
  2. Eine Ausnahme stellt nur die Installation von Docker auf einem RaspberryPi dar. Da dies über das Repository nicht funktioniert. Dann greife ich auf das Convenience Script zurück.
  3. Das Anpinnen einer bestimmten Docker-Version ist sinnvoll, allerdings versuche ich immer mit der aktuellen stabilen Version zu arbeiten. Derzeit ist das 19.03.5. Ich pinne die Version also in der Regel nicht an.

Auf der Debian-Maschine wird durch den Root-Login kein sudo genutzt. Das können wir also weglassen.

Zunächst wird der Paket-Index aktualisiert:

apt-get update

Als nächstes werden die Abhängigkeiten installiert:

apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg2 \
    software-properties-common

Jetzt lädt man den GPG-Schlüssel von Docker, um die Signierung des Pakets zu überprüfen:

curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -

Anschließend kann man den Fingerprint des Paketes überprüfen:

apt-key fingerprint 0EBFCD88

Nun wird dem Paket-Manager apt eine neue Quelle hinzugefügt, über die die Docker-Engine heruntergeladen werden kann:

add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/debian \
   $(lsb_release -cs) \
   stable"

Jetzt wird der Paket-Index erneut aktualisiert und Docker installiert:

apt-get update
apt-get install docker-ce docker-ce-cli containerd.io

Anschließend kann man einen ersten Hallo-Welt-Container starten, um die Funktionsfähigkeit von Docker zu überprüfen.

docker run hello-world

Erhält man eine passende Ausgabe, haben wir Docker erfolgreich installiert.

Nützliches

Ein paar nützliche Helfer dürfen es natürlich doch sein:

apt install fail2ban git htop nano mc rsync sudo unzip
  • fail2ban blockt ungewünschte SSH-Verbindungen
  • git kann man für die Arbeit mit Git-Repos immer gebrauchen
  • htop für die Anzeige der Auslastung in einer moderneren Darstellung als es top ermöglicht.
  • nano: Ja, ich nutze nano und nicht vi oder vim
  • mc: Manchmal finde ich die Darstellung des Midnight Commanders auch ganz praktisch.
  • rsync ist ideal, um Daten auch über die Grenzen von Maschinen hinweg zu bewegen
  • sudo macht für das Herabsetzen von Nutzerprivilegien Sinn.
  • Um mit gezippten Daten zu arbeiten, macht auch unzip Sinn.

Hierbei bringt jeder sicherlich noch seine persönlichen Präferenzen ein, aber diese Liste soll ein erster Anhaltspunkt sein.

Post-Installation Steps

Wie in der offiziellen Doku erwähnt, kann man nun auch noch weitere Maßnahmen ergreifen, sobald die Docker Engine installiert ist. Interessant finde ich die Einrichtung eines weiteren Systemnutzers, damit man nicht immer auf den Root-Nutzer zurückgreifen muss, wenngleich der zusätzliche Systemnutzer Zugriff auf den Docker-Socket bekommt und dadurch Root-ähnliche Rechte erhält. Docker rootless zu betreiben ist seit Längerem ein Thema. Hier kann man dazu Weiteres lesen.

Zurück zum Thema! Bei der Docker-Installation unter Debian wird bereits eine Gruppe "docker" angelegt. Es braucht also nur noch einen eigenen Systemnutzer und anschließend weisen wir dem die Docker-Gruppe zu.

adduser deploy
usermod -aG docker deploy

Anschließend sollte es möglich sein einen Hallo-Welt-Container als Nutzer "deploy" zu starten.

su deploy
docker run hello-world

Wenn man auch nun wieder wieder Ausgabe des Containers sehen kann, hat die Einrichtung funktioniert.

Kernel Tweaks

Docker setzt im Wesentlichen auf Features des Linux-Kernels, wie chroot, Namespaces, etc. Der Kernel ist somit essentiell für das Betreiben von Containern. Über die Jahre haben sich so einige Probleme mit Limits, die im Kernel gesetzt sind ergeben. Auf manche stößt man recht schnell. Beispielsweise beim Starten von Redis oder Elasticsearch, wird man darauf hingewiesen gewisse Einstellungen zu tätigen, um die volle Leistung nutzen zu können. Auf andere Probleme stößt man erst, wenn man die 10. MySQL-Instanz auf der Maschine starten möchte.

Es folgen meine Kernel Tweaks, die sich im Laufe der Jahre durch den langen Betrieb von Containern ergeben haben. Diese lege ich in einer eigenen Datei unter /etc/sysctl.d/80-docker.conf ab.

# Elasticsearch optimization
vm.max_map_count=262144

# MySQL optimization
fs.aio-max-nr=1048576

# Redis optimization
vm.overcommit_memory=1
net.core.somaxconn=65535

# Have a larger connection range available
net.ipv4.ip_local_port_range=1024 65000

# Reuse closed sockets faster
net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_fin_timeout=15

# The maximum number of "backlogged sockets".  Default is 128.
# net.core.somaxconn=4096
net.core.netdev_max_backlog=4096

# 16 MB per socket - which sounds like a lot, but will virtually never consume that much.
net.core.rmem_max=16777216
net.core.wmem_max=16777216

# Various network tunables
net.ipv4.tcp_max_syn_backlog=20480
net.ipv4.tcp_max_tw_buckets=400000
net.ipv4.tcp_no_metrics_save=1
net.ipv4.tcp_rmem=4096 87380 16777216
net.ipv4.tcp_syn_retries=2
net.ipv4.tcp_synack_retries=2
net.ipv4.tcp_wmem=4096 65536 16777216
#vm.min_free_kbytes=65536

# Connection tracking to prevent dropped connections (usually issue on LBs)
net.netfilter.nf_conntrack_max=262144
# Error by setting the following: cannot stat /proc/sys/net/ipv4/netfilter/ip_conntrack_generic_timeout: No such file or directory
net.ipv4.netfilter.ip_conntrack_generic_timeout=120
net.netfilter.nf_conntrack_tcp_timeout_established=86400

# ARP cache settings for a highly loaded Docker Swarm
net.ipv4.neigh.default.gc_thresh1=8096
net.ipv4.neigh.default.gc_thresh2=12288
net.ipv4.neigh.default.gc_thresh3=16384

System sauber halten

Trotz schlanker Container, wird beim kontinuierlichen Bauen und Ausrollen neuer Versionen das System doch schnell voll. Dafür bietet Docker die Prune-Befehle, die ich auch in diesem Artikel thematisiert habe, allerdings nutze ich die darin beschrieben automatischen Möglichkeiten so nicht mehr. Mittlerweile ist dafür ein eigenes Projekt entstanden, welches auf GitHub zu finden ist.

Beim Deployment von Containern setze ich auf den Swarm Mode und so starte ich den Service zum regelmäßigen Aufräumen der Maschine als Docker Stack.

Zunächst lade ich das Compose-File dazu in einen neuen Ordner:

mkdir -p /var/www/srv-cleanup
cd /var/www/srv-cleanup
wget https://raw.githubusercontent.com/servivum/docker-host-cleanup/master/docker-compose.production.yml

Nun initialisiere ich den Swarm Mode und starte den Stack:

docker swarm init
docker stack deploy -c docker-compose.production.yml srv-cleanup

Abschließend kann man noch einen Blick darauf werfen, ob der Dienst erfolgreich gestartet wurde, was natürlich etwas dauert, da zunächst noch das Container-Image aus dem Docker Hub geladen werden muss.

docker stack services srv-cleanup

Abschließende Gedanken

Um ein produktives System zu betreiben, reichen diese Maßnahmen natürlich noch nicht aus. Es braucht ein Backup-and-Restore-Konzept, sowie ein Monitoring, welches über den Zustand der Maschine berichtet.

Interessant ist zudem, wie man Web-Applikationen schnell und einfach mit HTTPS-Verschlüsselung online bringen kann. Diesem Thema widme ich mich in einem der kommenden Beiträge.

Bild von Taylor Vick