Tutorial Self-hosting mit Docker-Compose

  Tim Moritz   Lesezeit: 14 Minuten  🗪 13 Kommentare Auf Mastodon ansehen

Ein Leitfaden zum Hosten verschiedener Dienste mit Docker-Compose

tutorial self-hosting mit docker-compose

In unseren Artikelvorschlägen kam der Wunsch, eine Reihe über Self-hosting mit Docker-Compose zu schreiben. Außerdem kam der Wunsch einen Artikel über "Docker Services erreichen mittels Traefik" zu schreiben. Da die Vorgehensweise beim Aufsetzen neuer Services mit Docker-Compose eigentlich immer sehr ähnlich ist, habe ich mich für ein zweiteiliges Tutorial entschieden. In diesem ersten Teil werde ich auf das aufsetzen von Diensten mittels Doccker-Compose anhand verschiedener Beispiele eingehen. Im zweiten Teil sollen diese Beispiele dann um Traefik als Reverse-Proxy erweitert werden. Dazu hatte ich bereits einen Artikel geschrieben. Da aber auch ich nie auslerne, wird die neue Version noch einfacher umzusetzen sein und ich werde noch auf ein paar zusätzliche Aspekte eingehen.


Wenn ich hier über Dienste spreche, dann rede ich über Serveranwendungen, die in der Regel über einen TCP-Port erreichbar sind (manchmal auch mehrere). Bei Anwendungen mit einem Webinterface ist das normalerweise Port 80 (HTTP) und Port 443 (HTTPS), was wir im Browser nur anhand des verwendeten Protokolls sehen können. Dennoch entspricht ein http://gnulinux.ch im Browser eigentlich http://gnulinux.ch:80 . Da wir einen Port immer nur einmal verwenden können und ein :12345 im Browser nicht schön aussieht, empfiehlt es sich einen Reverse-Proxy zu nutzen. Außerdem möchte man im Internet auch verschlüsselte Verbindungen (HTTPS) nutzen und mit einem Reverse-Proxy lassen sich die nötigen Zertifikate bequem zentral verwalten. Darum soll es im zweiten Teil dieses Tutorials gehen, hier beschränken wir uns auf die Erreichbarkeit über einen beliebigen Port.

Beispiele für solche Dienste mit Webinterface sind paperless-ngx, metabase, uptime-kuma oder Nextcloud. Die hier gezeigten Schritte funktionieren aber genauso für Dienste ohne Webinterface wie ein Mailserver, eine HTTP basierte API, ein (S)FTP Dienst oder ein Datenbank-Server. Ein Server ist dafür nicht erforderlich, denn das ganze kann auch auf dem Desktop ausgeführt werden. Bspw. um an einem Projekt mitzuentwickeln oder weil man es nicht dauerhaft am Laufen braucht (so handhabe ich das bspw. mit paperless-ngx).

Generelle Vorgehensweise

Immer vorausgesetzt es gibt ein Docker-Image für die gewünschte Anwendung, muss man sich für das Hosten mit Docker-Compose noch mit folgenden Punkten beschäftigen:

- Erreichbarkeit im Netzwerk
- Wie wird die Anwendung konfiguriert?
- Wo werden Daten gespeichert?
- Aus welchen (zusätzlichen) Komponenten (bspw. Datenbank) besteht die Anwendung?

Bei Punkt 4 wird man die Punkte 1 -3 für die zusätzlichen Komponenten auch prüfen müssen. Diese Punkte sollten schon ausreichen, um die Applikationen erfolgreich zum Laufen zu bringen. Je nach Vorhaben sollte man sich aber noch mit Themen wie logging und monitoring auseinandersetzen, auf die ich hier nicht weiter eingehe.

Diese 4 Punkte werden uns durch das Tutorial begleiten. Gute Informationsquellen sind meistens die Projektdokumentation, die Seite des Docker Images auf Docker Hub oder das dazugehörige Code Repository. In einigen Projekten findet man eine Anleitung für Docker-Compose, in anderen nur für Docker direkt. Letztere gibt aber in gibt uns aber Hinweise darauf, wie wir das Ganze in Docker-Compose umsetzen können. In Projekten mit mehreren Komponenten findet man meist auch eine docker-compose.yml im Code Repository, an der man sich gut orientieren kann.

Praxis-Beispiel

Fangen wir ganz vorne an und installieren Docker und Docker-Compose auf einer frischen Debian 12 Installation. In diesem Fall hat unser Server die IP 10.30.0.22 und wir sind per SSH mit einem User ohne root-Rechte, aber mit sudo-Rechten verbunden. Nach der Installation fügen wir den aktuellen User noch der Gruppe docker hinzu und loggen uns neu ein, damit die Änderung wirksam wird.

sudo apt install docker docker-compose
sudo usermod -aG docker $USER
su - $USER

Ein kurzer Test ob Docker läuft:

docker ps

Das Ergebnis sollte eine leere Liste mit Überschriften sein. Dann kann es jetzt losgehen.

Erste Anwendung

Als ersten Dienst setzen wir das Tool Invidious Router auf. Da das Tool keine Daten speichert und nur aus einem Docker-Image bzw. einer Komponente besteht, brauchen wir hier nur Punkt 1 und 2 betrachten. Auf der Docker Hub Seite zum Image werden und diese beiden Punkte auch schon indirekt beantwortet. Wir können sehen, dass Port 8050 benutzt wird und dass die Konfiguration in einer config.yaml im Ordner /app/config innerhalb des Containers liegt. Diese brauchen wir aktuell nicht anpassen, wollen Sie aber trotzdem von außerhalb des Docker-Containers zugängig machen.

Ich lege jetzt im Homeverzeichnis einen Ordner für das Projekt und darin einen für die Konfiguration an. In den Projektordner kommt dann gleich unsere docker-compose.yml.

mkdir ~/invidious-router
cd ~/invidious-router
mkdir config
touch docker-compose.yml

Mit dem Editor der Wahl füllen wir die docker-compose.yml jetzt mit folgendem Inhalt:

version: '3.8'

services:
  invidious-router:
    image: gaincoder/invidious-router
    restart: unless-stopped
    ports:
      - 8888:8050
    volumes:
      - ./config:/app/config

Wir teilen Docker-Compose jetzt also mit welches Image wir laden wollen, dass der Port 8888 unseres Servers auf Port 8050 des Containers geroutet werden soll und dass unser config-Ordner auf dem Server den Ordner /app/config innerhalb des Containers ersetzen soll. Zusätzlich geben wir als Restart-policy noch mit, dass unser Container automatisch gestartet werden soll, wenn bspw. der Docker-Dienst oder der ganze Server neu gestartet wird. Mit

docker-compose up -d

starten wir das Ganze und schon ist unser erster Service am Start. Nochmal kontrollieren ob alles läuft mittels

docker-compose ps

Mit Kenntnis über IP und Port können wir unseren Service jetzt aufrufen:

http://10.30.0.22:8888

Da der Invidious Router uns direkt weiterleitet, nochmal ein zweiter Test (siehe Dokumentation)

http://10.30.0.22:8888/router-status

Zweite Anwendung

Nun wollen wir als zweiten Service Uptime Kuma aufsetzen. Hierbei gilt es jetzt die ersten 3 Punkte zu beachten, denn die Applikation speichert Daten, die nicht verloren gehen sollen. Kümmern wir uns darum nicht, werden die Daten nur innerhalb des Containers gespeichert und gehen bspw. bei einem Update einfach verloren.

Ein Blick auf die Website des Projekts oder auf die Docker Hub Seite des Images lässt uns schnell die Fragen 1, 3 und 4 beantworten. Es ist nur eine Komponente, der Container verwendet Port 3001 und speichert seine Daten in /app/data. Ein Hinweis auf eine Konfigurationsdatei oder Umgebungsvariablen zur Konfiguration finden sich nicht, also müssen wir davon ausgehen, dass die Konfiguration ebenfalls in /app/data gespeichert wird und haben damit Frage 2 beantwortet.

Wie beim letzten Service auch, legen wir für den Service wieder eine Ordnerstruktur an.

mkdir ~/uptime-kuma
cd ~/uptime-kuma
mkdir data
touch docker-compose.yml

In der Projektdokumentation findet man auch eine Vorlage für die docker-compose.yml, die wir 1:1 übernehmen könnten, wir orientieren uns aber mehr an der, im vorigen Service bereits erstellten, Datei.

version: '3.8'

services:
  uptime-kuma:
    image: louislam/uptime-kuma
    restart: unless-stopped
    ports:
      - 3001:3001
    volumes:
      - ./data:/app/data

Nach einem

docker-compose up -d

sollte uns unter

http://10.30.0.22:3001/

das Webinterface begrüßen.

Dritte Anwendung

Als dritten Service setzen wir paperless-ngx auf. Auf der Docker Hub Seite des Images suchen wir vergeblich nach einem docker run Befehl, wie in den vorherigen Beispielen. Das liegt daran, dass hier nun Punkt 4, also mehrere Komponenten ins Spiel kommen. Im Bereich Getting Started wird auf eine Liste verschiedener Docker-Compose Setups verlinkt. Verschiedene, weil paperless-ngx mit mehreren Datenbank-Typen umgehen kann. Wir übernehmen jetzt einfach mal 1:1 die docker-compose.mariadb.yml.

version: "3.4"
services:
  broker:
    image: docker.io/library/redis:7
    restart: unless-stopped
    volumes:
      - redisdata:/data

  db:
    image: docker.io/library/mariadb:10
    restart: unless-stopped
    volumes:
      - dbdata:/var/lib/mysql
    environment:
      MARIADB_HOST: paperless
      MARIADB_DATABASE: paperless
      MARIADB_USER: paperless
      MARIADB_PASSWORD: paperless
      MARIADB_ROOT_PASSWORD: paperless

  webserver:
    image: ghcr.io/paperless-ngx/paperless-ngx:latest
    restart: unless-stopped
    depends_on:
      - db
      - broker
    ports:
      - "8000:8000"
    volumes:
      - data:/usr/src/paperless/data
      - media:/usr/src/paperless/media
      - ./export:/usr/src/paperless/export
      - ./consume:/usr/src/paperless/consume
    env_file: docker-compose.env
    environment:
      PAPERLESS_REDIS: redis://broker:6379
      PAPERLESS_DBENGINE: mariadb
      PAPERLESS_DBHOST: db
      PAPERLESS_DBUSER: paperless # only needed if non-default username
      PAPERLESS_DBPASS: paperless # only needed if non-default password
      PAPERLESS_DBPORT: 3306

volumes:
  data:
  media:
  dbdata:
  redisdata:

Wir gehen die Datei anhand unserer 4 Punkte durch:

Erreichbarkeit im Netzwerk:

Webinterface über den Container-Port 8000 wird auf dem Server ebenfalls auf Port 8000 verfügbar gemacht. Alle Services innerhalb einer docker-compose.yml erstellen automatisch ein gemeinsames internes Netzwerk und können so untereinander kommunizieren.

Wie wird die Anwendung konfiguriert?

In allen 3 Komponenten finden wir im Bereich "environment" Umgebungsvariablen zur Konfiguration. Hier ersetzen wir die Standard-Zugangsdaten. Des Weiteren wird beim "webserver" eine "env_file" angegeben. Diese Datei befindet sich auch in der Liste und beinhaltet noch mehr (auskommentierte) Umgebungsvariablen.

Wo werden Daten gespeichert?

Alle 3 Services, die paperless-ngx benötigt, speichern Daten. Innerhalb der Container sind die Pfade bspw. /var/lib/mysql für den Service "db" und /data für den Service "broker". Die Hauptanwendung, hier "webserver" genannt, hat sogar 4 Pfade, wo Daten abgelegt werden. Anhand des ./ erkennen wir, dass nur die Ordner export und consume auf dem Dateisystem unseres Servers benutzt werden. Bei allen anderen wurden hier sog. Volumes benutzt. Dies sind quasi virtuelle Festplatten, die nur innerhalb von Docker verfügbar sind. Diese werden einmal im Bereich "volumes" definiert (hier mit den Standard-Einstellungen) und können dann anstelle eines Pfades verwendet werden. Daten auf Volumes überstehen auch ein Update oder ähnliches.

Aus welchen (zusätzlichen) Komponenten (bspw. Datenbank) besteht die Anwendung?

Zusätzlich zur Hauptanwendung "webserver" werden hier noch 2 Komponenten/Services benutzt. Einmal ein "broker", welches hier eine Redis-Instanz ist und einmal eine "db", also eine Datenbank, vom Typ MariaDB. Dem Webserver wird mit "depends_on" gesagt, dass zuerst die beiden anderen Services hochgefahren sein müssen, bevor er starten kann.

Legen wir also wieder unsere Ordnerstruktur und Dateien an

mkdir ~/paperless-ngx
cd ~/paperless-ngx
mkdir export
mkdir consume
touch docker-compose.env
touch docker-compose.yml

und kopieren die Inhalte von docker-compose.env und docker-compose.mariadb.yml in die frisch angelegten Dateien.

Nach dem letzten Schritt

docker-compose up -d

sollte uns unter

http://10.30.0.22:8000

nach kurzer Zeit das Webinterface von unserer paperless-ngx Instanz begrüßen.

Erstes Ziel erreicht

Wir hosten nun 3 Anwendungen in unterschiedlichen Programmiersprachen (Go, NodeJS, Python) auf unserem Server. Außerdem laufen noch ein MariaDB- und ein Redis-Dienst. Sofern unser Server und die entsprechenden Ports (ggf. Firewall beachten) aus dem Internet erreichbar sind, kann mit diesen Anwendungen aus dem Netz gearbeitet werden. Von den unterschiedlichen Programmiersprachen haben wir nichts bemerkt und auf dem Server direkt nur docker, docker-compose und deren Abhängigkeiten installiert. Jede der Anwendungen läuft in Isolation und kann rückstandslos wieder entfernt werden. Die Vorgehensweise sollte sich auf nahezu alle Webanwendungen, die ein einigermaßen dokumentiertes Docker Image haben, übertragen lassen. Im zweiten Teil werden wir dann noch eine Domain und SSL-Zertifikate verwenden, um das ganze abzurunden. 

Bildquelle: https://pixabay.com/de/photos/container-lagerung-handeln-4203677/

Tags

Docker, Self-Hosting, Tutorial

Tealk
Geschrieben von Tealk am 2. Mai 2024 um 15:31

Hi,

schau dir mal https://github.com/louislam/dockge an, das könnte dir dann auch gefallen wenn du gern mit docker-compose arbeitest.

Tim Moritz Admin
Geschrieben von Tim Moritz am 2. Mai 2024 um 15:39

Das sieht interessant aus, werde ich auf jeden Fall mal ausprobieren! Wenn du dich damit auskennst, freue ich mich schon auf deinen Artikel hier :)

CL
Geschrieben von CL am 3. Mai 2024 um 09:15

Hi,

zwei kleine Hinweise. Die Versionsangabe in der docker-compose.yml ist deprecated und kann entfallen. Und docker compose ist inzwischen direkter Bestandteil von DOcker, so dass docker-compose nicht extra installiert werden muss. Der Aufruf erfolgt dann ohne Bindestrich: docker compose up -d

Udo M.
Geschrieben von Udo M. am 3. Mai 2024 um 16:05

Bei Podman (was man m. M. nach als Alternative benutzen sollte) ist aber auch noch docker-compose up -d ...

JoCa
Geschrieben von JoCa am 3. Mai 2024 um 09:23

Hallo Tim,

vielen Dank für den sehr schönen Artikel, hat mir viele Fragen super beantwortet! Herzlichen Dank!

V wie Vendetta
Geschrieben von V wie Vendetta am 3. Mai 2024 um 13:22

Können diese Container-Lösungen (docker, podman, nomad, kubernetes etc.) mittlerweile mit dem aktuellen Standard nftables umgehen? Ich hab mich mal umgesehen und die Antwort scheint "nein" zu sein. Unterstützung sei geplant.

Benno
Geschrieben von Benno am 4. Mai 2024 um 07:20

Vielen Dank für diesen spannenden Beitrag. Er ist sehr gut gegliedert und nachvollziehbar. Wäre er vor 3 Monaten erschienen, hätte er mir einige schlaflose Nächte erspart. ;-)

Benno
Geschrieben von Benno am 6. Mai 2024 um 00:45

Hallo Tim

Danke für den Beitrag. Ich habe versucht, die 3 Beispiele auf einem Raspi Version 4 zum laufen zu kriegen.

Invidious Router: das "Pull" war problemlos aber endlose "restarting"-Schleife. Keine Verbidnung zur Webseite möglich. Weisst du, woher das komment kann? Uptime Kuma : funktionniert problemlos paperless-ngx: unmöglich auf einem Raspi, laufen zu lassen:

  1. docker.io/library/mariadb:10 bietet keine ARM-Version des Dockers
  2. ghcr.io/paperless-ngx/paperless-ngx : ditto

Bei MAriaDB habe ich auf Guthub einen anderen Quellcode gefunden, der bei ARM-Prozessoren läuft. Bei paperless-ngx habe ich keine andere Alternartive für ARM-Prozessoren gefunden.

Hast du vielleicht einen Tipp, wie man das Ganze auf raspi zum Laufen kriegt? Auf welcher Plattform hast du deine Beispiele zum Laufen gekriegt?

Danke, Gruss, Benno

Tim Moritz Admin
Geschrieben von Tim Moritz am 6. Mai 2024 um 09:42

Hallo Benno,

Ich hatte das ganze auf einem x86 Debian 12 gemacht. Für den Invidious Router muss ich bei Zeiten mal ein ARM Image bereitstellen (ist ja von mir das Projekt), bei paperless-ngx müsste es aber entsprechende ARM Images geben, sowohl von der Software selbst als auch von den Komponenten.

Helfy
Geschrieben von Helfy am 6. Mai 2024 um 13:18

Für paperless-ngx und MariaDB kann man mindestenszwischen linux/amd64 und linux/arm64 wählen (Stichwort: digest): https://hub.docker.com/r/paperlessngx/paperless-ngx/tags

Benno
Geschrieben von Benno am 7. Mai 2024 um 01:16

Danke beide, für Euer Feedback.

@Helfy: eine Laienfrage: Der Befehl 'uname -m' gibt mir 'aarch64' zurück. Es ist also eine 64-Bits Version vom Raspberry Pi OS, die auf meinem Raspi 4B installiert ist.

Beim PULL vom 'latest' Build von paperless ngx erhalte ich aber die Fehlermeldung: ERROR: no matching manifest for linux/arm/v8 in the manifest list entries

Erst bei dem alten Build 1.17 mit 'linux/arm/v7' support funktioniert die Installation problemlos.

Weisst du vielleicht, voran das liegen könnte?

Danke, Gruss, Benno

Helfy
Geschrieben von Helfy am 7. Mai 2024 um 08:02

docker pull ghcr.io/paperless-ngx/paperless-ngx:2.8.1@sha256:968ed6343fa2127be80cbbf41e101fee66cfa5082983c6654a891c21406c5c80 oder in docker-compose.yml: webserver: image: ghcr.io/paperless-ngx/paperless-ngx:2.8.1@sha256:968ed6343fa2127be80cbbf41e101fee66cfa5082983c6654a891c21406c5c80

für das zur Zeit aktuellste Image für linux/arm64 (für Updates muss die Angabe dann geändert werden, das geht bestimmt auch eleganter)

Ich habe leider kein Raspberry Pi zum Testen parat, ansonsten: https://docs.paperless-ngx.com/faq/#will-paperless-ngx-run-on-raspberry-pi

Benno
Geschrieben von Benno am 8. Mai 2024 um 02:15

Danke Helfy für den Hinweis.

Kleine Korrektur: erst mit folgendem Digest hat das Download/PULL in Docker-compose auf dem Raspi/ARM64 funktioniert: webserver: image: ghcr.io/paperless-ngx/paperless-ngx:2.8.1@sha256:1705a8a9e097f1b8609fa7cc43f6f536a38f2f02e22c7a45cf53c15550f0b612

Diese Version konnte auf dem Raspi installiert werden. Nach dem Start des Containers verhältet sie sich aber nicht stabil: IMAGE: ghcr.io/paperless-ngx/paperless-ngx:2.8.1
STATUS: Restarting (159) 3 seconds ago

Was kann dagegen unternommen werden?

Danke, Benno