Ein Docker‑Host ist schnell installiert – aber ohne sauberes Netzwerk- und Betriebsmodell wird er später unübersichtlich: Port‑Kollisionen, „wo läuft was?“, und Workarounds, die sich in jeder App‑Compose wiederholen.

Dieser Artikel setzt deshalb bewusst früh an: Docker als Plattform, Compose als reproduzierbarer Workflow und ein gemeinsames Netzwerk (homelab_net), das Container intern verbindet und extern nur einen kontrollierten Einstiegspunkt freigibt.

Jeder nach außen geöffnete Port ist eine zusätzliche Angriffsfläche. Statt pro Anwendung am Host Ports zu veröffentlichen, schaffen wir eine Struktur, die später über einen klaren, kontrollierten Einstiegspunkt (Caddy auf 80/443) geführt wird.

Am Ende hast du

  • Docker Engine installiert und laufend (Debian 13).
  • Compose v2 als Plugin installiert (docker compose ...).
  • Eine saubere Basisstruktur für Stacks unter /srv/docker.
  • Ein gemeinsames internes Netzwerk (homelab_net) für alle Container
  • Einen minimalen „Netzwerk-Test“
  • Klarheit, welche Ports am Host offen sind (jetzt: SSH; später: 80/443 für Caddy).

Voraussetzungen

  • Debian 13 minimal installiert.
  • User martin (UID/GID 1000:1000), SSH-Key-Login.
  • UFW aktiv, SSH offen.

1. Was ist Docker?

Docker ist hier nicht „ein Tool mehr“, sondern die Methode, Dienste gekapselt zu betreiben: Abhängigkeiten, Runtime und Startlogik sind im Container definiert, während Daten in Volumes liegen. Das Ziel ist, dass du einen Dienst jederzeit reproduzierbar starten kannst – ohne dass der Host „zugemüllt“ wird.

Compose v2 ergänzt das um einen Projekt‑Workflow: Mehrere Services, Netzwerke und Volumes werden als YAML beschrieben und als Einheit verwaltet.

In einem VPS-Setup finden sich Services intern über ihren Service-Namen, nicht über feste LAN-IPs. Du denkst also nicht mehr in IP-Adressen, sondern in Dienstenamen wie vaultwarden, immich oder paperless.

2. Docker + Compose v2 installieren

Docker empfiehlt für Debian die Installation über das offizielle APT‑Repository

2.1 Docker GPG‑Key anlegen

sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

Hier richten wir den offiziellen Docker-Signaturschlüssel ein. Damit kann dein System prüfen, ob Pakete wirklich von Docker stammen und nicht manipuliert wurden.

2.2 Repository hinzufügen (Debian Trixie)

Debian 13 trägt den Namen Trixie. Docker nutzt diesen Codenamen zur Auswahl des passenden Repository-Zweigs.

sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<'EOF'
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: trixie
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF

sudo apt update

Damit weiß APT jetzt, wo es die offiziellen Docker-Pakete findet.

2.3 Prüfen, ob das Docker-Repo aktiv ist

Wir prüfen, ob APT die Docker-Pakete vom offiziellen Repo (nicht von Debian) bezieht:

apt-cache policy docker-ce

Erwartung:

Candidate: 5:27.x.x~3-0~debian-trixie
       500 https://download.docker.com/linux/debian trixie/stable

Wichtig: Die URL muss download.docker.com enthalten (nicht deb.debian.org).

2.4 Docker Engine + Compose v2 installieren

sudo apt install -y \
  docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin

Damit installierst du:

Paket Bedeutung
docker-ce Die Docker Engine selbst
docker-ce-cli Das Docker Kommandozeilen-Tool
containerd.io Laufzeitumgebung für Container
docker-buildx-plugin Modernes Build-System
docker-compose-plugin Compose v2 als Docker-Plugin

2.5 Dienst prüfen

Docker startet den Dienst normalerweise automatisch. Wir prüfen jetzt, ob alles korrekt läuft.

sudo systemctl status docker --no-pager
sudo docker run --rm hello-world
docker compose version

Docker sollte active (running) sein, hello-world läuft durch und docker compose version gibt eine Version aus.

2.6 Docker ohne sudo nutzen

Standardmäßig darf nur root Docker steuern. Damit dein normaler Benutzer Container verwalten darf, fügen wir ihn der docker-Gruppe hinzu.

sudo usermod -aG docker martin

Gruppenrechte werden erst beim nächsten Login aktiv. Deshalb musst du dich einmal neu verbinden:

exit
ssh martin@<SERVER-IP>

Danach prüfen:

groups martin
docker ps

Wenn kein Fehler erscheint, kannst du Docker nun ohne sudo verwenden.

2.7 Docker-Log-Rotation aktivieren

Standardmäßig schreibt Docker Container-Logs in Dateien, die unbegrenzt wachsen. Ein gesprächiger Container kann auf einem VPS damit innerhalb weniger Wochen die komplette Disk füllen – und ein voller Server reagiert nicht mehr.

Das verhindern wir einmalig mit einer kleinen Konfigurationsdatei:

sudo nano /etc/docker/daemon.json

Inhalt einfügen:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

Speichern (Ctrl+X, dann Y) und Docker neu laden:

sudo systemctl restart docker

Was passiert hier?

Pro Container werden maximal 3 Log-Dateien à 10 MB aufbewahrt – also höchstens 30 MB pro Container. Sobald eine Datei voll ist, wird rotiert; die älteste fällt raus. Das reicht für Fehlersuche im Alltag und schützt gleichzeitig vor vollen Disks.

Check

docker info | grep -i "logging driver"

Erwartung: Logging Driver: json-file

Warum gleich am Anfang?

Die Einstellung gilt nur für Container, die nach der Konfigurationsänderung gestartet werden. Wenn du sie erst später setzt, müsstest du alle bestehenden Container neu erstellen. Jetzt einmal eingerichtet – und alle zukünftigen Container haben automatisch sinnvolle Log-Grenzen.

3. Persistente Datenbasis anlegen (/srv/docker)

Auf einem VPS gibt es in der Regel nur eine Systemdisk. Deshalb brauchen wir einen klaren Ort für alle containerbezogenen Daten.

Wir nutzen dafür: /srv/docker

Dieser Pfad ist im Linux-Standard (FHS) für „Server-Daten" vorgesehen – also genau das, was unsere Container später speichern.

Verzeichnis anlegen

sudo mkdir -p /srv/docker/stacks
sudo chown -R martin:martin /srv/docker

Die erste Zeile legt das Verzeichnis als root an, weil /srv selbst dem System gehört. Die zweite Zeile übergibt die Eigentümerschaft von /srv/docker an deinen User. Damit kannst du später Konfigurationsdateien in Stack-Verzeichnissen ohne sudo bearbeiten – auch bequem per VS Codium oder einem anderen Editor über SSH. Die Container selbst sind davon nicht betroffen, denn Docker läuft als root über den Docker-Socket.

Username anpassen

Falls dein Benutzer nicht martin heißt, ersetze ihn in beiden chown-Argumenten durch deinen tatsächlichen Usernamen (den, mit dem du eingeloggt bist – whoami zeigt ihn dir).

Was hier später landet

Pfad Zweck
/srv/docker/stacks/ Hier liegen später deine einzelnen Docker-Projekte
./data / ./config innerhalb eines Stacks Container-Daten und Konfigurationen

Wichtig: Hier liegen nur Laufzeitdaten der Container, keine Dokumentation oder Notizen.

Prüfen

ls -la /srv/docker
df -h /srv/docker

Erwartung: Das Verzeichnis existiert, gehört dem Benutzer martin und es ist ausreichend Speicherplatz vorhanden.

4. Homelab-Netzwerk erstellen

Alle Container dieser Serie nutzen ein gemeinsames internes Netzwerk: homelab_net.

docker network create homelab_net

Prüfen

docker network ls | grep homelab

Erwartung:

homelab_net   bridge    local

Warum ein gemeinsames Netz?

Statt für jede App ein eigenes Netz zu erstellen, nutzen wir ein zentrales homelab_net. Das vereinfacht die Verwaltung und ermöglicht Service-Discovery zwischen allen Diensten:

  • Caddy (Reverse Proxy) kann alle Apps erreichen
  • Container finden sich über Service-Namen (http://vaultwarden:80)
  • Einfacher für Einsteiger – ein Netz, keine komplexe Isolation

Sicherheit: Die echte Isolation kommt durch UFW (nur Port 22/80/443 offen), Tailscale VPN (Admin-Tools) und Authelia (2FA für Apps) – nicht durch Netzwerk-Trennung.

Fortgeschritten: Pro-Stack-Isolation (optional)

Für maximale Sicherheit kannst du später jeder App ein eigenes Netz geben:

  • vaultwarden_net (nur Vaultwarden + Caddy)
  • immich_net (nur Immich + Caddy)

Vorteil: Immich kann Vaultwarden-Datenbank nicht erreichen.

Nachteil: Mehr Komplexität – Caddy muss in alle Netze.

Für diese Einsteiger-Serie nutzen wir bewusst ein gemeinsames Netz.

5. Netzwerk-Test: Service-Discovery

Wir testen jetzt nicht einfach „ob Docker läuft", sondern ob sich Container im selben Netzwerk über ihren Servicenamen erreichen können.

Genau dieses Prinzip nutzt später Caddy, um Anfragen intern an andere Dienste weiterzuleiten.

Test-Stack anlegen

mkdir -p /srv/docker/stacks/test
cat > /srv/docker/stacks/test/compose.yml << 'EOF'
name: test

services:
  whoami:
    image: traefik/whoami
    networks:
      - homelab

  curl:
    image: curlimages/curl:latest
    command: ["sh","-c","curl -sS http://whoami:80 | grep -E 'Hostname|RemoteAddr'"]
    networks:
      - homelab
    depends_on:
      - whoami

networks:
  homelab:
    external: true
    name: homelab_net
EOF

Was passiert hier?

  • name: test – Projekt-Name (explizit gesetzt, unabhängig vom Verzeichnis)
  • Service whoami – antwortet auf HTTP-Anfragen
  • Service curl – fragt http://whoami:80 ab (über Service-Namen!) und filtert die zwei aussagekräftigen Zeilen heraus
  • Netzwerk homelab – nutzt das globale homelab_net

Test starten

Wir starten whoami als Hintergrund-Dienst und schicken dann curl einmalig dagegen:

cd /srv/docker/stacks/test
docker compose up -d whoami
docker compose run --rm curl
  • up -d whoami startet nur den whoami-Dienst im Hintergrund.
  • run --rm curl führt den curl-Container genau einmal aus, zeigt das Ergebnis und entfernt ihn danach automatisch (--rm).

Erwartung: Du siehst genau zwei Zeilen:

Hostname: 1f8b2c6bd0eb
RemoteAddr: 172.18.0.3:54972

Das zeigt: curl hat whoami allein über den Service-Namen erreicht – ohne IP-Adresse. Die RemoteAddr-Zeile ist die interne homelab_net-Adresse, von der die Anfrage kam. Genau dieses Prinzip nutzt später Caddy.

Projekt-Status prüfen

docker compose ls

Erwartung:

NAME    STATUS              CONFIG FILES
test    running(1)          /srv/docker/stacks/test/compose.yml

running(1) = der whoami-Dienst läuft noch (der curl-Container wurde durch --rm bereits entfernt). Der angezeigte Projekt-Name test kommt aus name: test ganz oben in der compose.yml.

Aufräumen

docker compose down
cd ~
rm -rf /srv/docker/stacks/test

Compose-Projekt-Name verstehen

Jedes Compose-Projekt braucht einen Namen. Du hast zwei Optionen:

  1. Automatisch – Compose nutzt den Verzeichnisnamen (test aus /srv/docker/stacks/test)
  2. Explizit – Du setzt name: ganz oben in compose.yml

Für diese Serie nutzen wir explizite Namen:

name: vaultwarden  # ← Immer ganz oben

services:
  vaultwarden:
    ...

Warum? Das macht deine Stacks unabhängig vom Verzeichnisnamen und reproduzierbar.

Die drei Namen in Compose

Begriff Zweck Beispiel Wo gesetzt?
Projekt-Name Gruppierung aller Services vaultwarden name: (ganz oben)
Service-Name Interner DNS-Name vaultwarden services: (Schlüssel)
Container-Name Name in docker ps vaultwarden container_name: (optional)

Beispiel:

name: vaultwarden  # ← Projekt-Name

services:
  vaultwarden:     # ← Service-Name (für http://vaultwarden:80)
    container_name: vaultwarden  # ← Container-Name (in docker ps)

Tipp

Für einfache Setups sind alle drei Namen identisch – das reduziert Verwirrung.

Für Routing nutzt Docker den Service-Namen, nicht den Container-Namen. Caddy spricht später http://vaultwarden:80 an (nicht http://vw-prod:80, selbst wenn du container_name: vw-prod gesetzt hast).

6. Docker & Firewall – das Sicherheitsprinzip

Wenn du in Compose ports: definierst, ist der Port sofort am Host erreichbar – auch wenn UFW aktiv ist.

Beispiel:

services:
  nextcloud:
    ports:
      - "8080:80"  # ← Port 8080 ist jetzt öffentlich!

Viele gehen davon aus, dass UFW das blockiert. Das ist nicht zuverlässig, weil Docker eigene Netzwerkregeln setzt.

Unsere Lösung: Nur ein öffentlicher Einstiegspunkt

Regel für diese Serie:

  • Apps veröffentlichen KEINE Ports am Host (ports: wird nicht genutzt)
  • Öffentlich erreichbar ist später nur Caddy (Port 80/443)
  • Caddy routet intern zu den Apps (über homelab_net)

Vorteil:

Ohne Reverse Proxy Mit Reverse Proxy (Caddy)
Nextcloud: Port 8080 cloud.example.de → Caddy → nextcloud:80
Gitea: Port 3000 git.example.de → Caddy → gitea:3000
Paperless: Port 8000 docs.example.de → Caddy → paperless:8000
Offen: 22, 8080, 3000, 8000 Offen: 22, 80, 443

Außen nur ein Eingang, innen beliebig viele Dienste.

Adressierung: Service-Namen statt IPs

Statt in IP-Adressen zu denken, nutzt du Service-Namen:

  • vaultwarden (statt 172.18.0.5)
  • immich (statt 172.18.0.8)
  • paperless (statt 172.18.0.12)

Caddy spricht später intern einfach http://vaultwarden:80 an.

Zugriff später über Domains (Vorschau)

Von außen greifst du über Subdomains zu:

  • cloud.deine-domain.de → Caddy → nextcloud:80
  • git.deine-domain.de → Caddy → gitea:3000
  • docs.deine-domain.de → Caddy → paperless:8000

Wichtig: Container brauchen keine öffentlichen Ports – Domains steuern den Weg nach innen.

7. Best Practices für Docker auf VPS

  • Sprechende Service-Namenvaultwarden, nicht app-v2
  • Explizite Projekt-Namenname: ganz oben in compose.yml
  • Nur ein öffentlicher Einstiegspunkt – Caddy auf 80/443
  • Routing über Domainscloud.example.de, nicht :8080
  • Ein gemeinsames Netzwerkhomelab_net für alle Apps
  • ports: ist die Ausnahme – Standard ist intern-only
  • Container starten automatisch wieder – in allen App-Compose-Files dieser Serie steht restart: unless-stopped. Damit kommen Container nach einem Server-Reboot oder Crash automatisch wieder hoch, stoppen aber sauber bei einem expliziten docker compose down.

In dieser Serie gilt: ports: ist die Ausnahme, nicht die Regel.

Und jetzt?

Du hast jetzt eine stabile Docker-Plattform auf deinem VPS:

  • Docker Engine läuft
  • Compose v2 ist einsatzbereit
  • Log-Rotation ist aktiv – keine vollen Disks durch wuchernde Container-Logs
  • homelab_net verbindet alle Container intern
  • Container kommunizieren intern über Service-Namen
  • Es sind keine unnötigen Ports nach außen offen

Damit ist die technische Basis gelegt.

Im nächsten Schritt hängt es davon ab, was du vorhast:

  • Wenn du später AdGuard Home oder Pi-hole für netzwerkweites Adblocking betreiben willst: Artikel 04 zeigt die nötige DNS-Vorbereitung.
  • Für alle anderen Setups (Vaultwarden, Immich, Paperless, Ghost …): Direkt weiter mit Artikel 05 – Tailscale VPN für sicheren Admin-Zugang ohne offene Ports.

Docker bleibt dabei unverändert.

👉 Nächster Artikel: 04. DNS-Vorbereitung (optional) oder direkt 05. Tailscale VPN einrichten