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
martinheißt, ersetze ihn in beidenchown-Argumenten durch deinen tatsächlichen Usernamen (den, mit dem du eingeloggt bist –whoamizeigt 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– fragthttp://whoami:80ab (über Service-Namen!) und filtert die zwei aussagekräftigen Zeilen heraus - Netzwerk
homelab– nutzt das globalehomelab_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 whoamistartet nur denwhoami-Dienst im Hintergrund.run --rm curlführt dencurl-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:
- Automatisch – Compose nutzt den Verzeichnisnamen (
testaus/srv/docker/stacks/test) - Explizit – Du setzt
name:ganz oben incompose.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:80an (nichthttp://vw-prod:80, selbst wenn ducontainer_name: vw-prodgesetzt 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:80git.deine-domain.de→ Caddy →gitea:3000docs.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-Namen –
vaultwarden, nichtapp-v2 - Explizite Projekt-Namen –
name:ganz oben incompose.yml - Nur ein öffentlicher Einstiegspunkt – Caddy auf 80/443
- Routing über Domains –
cloud.example.de, nicht:8080 - Ein gemeinsames Netzwerk –
homelab_netfü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 explizitendocker 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_netverbindet 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
Discussion