*Stand: November 2026
Ab diesem Artikel läuft jede Anwendung auf deinem Server in einem Container: einem abgeschlossenen Paket, das die Anwendung samt allem mitbringt, was sie zum Laufen braucht. Die Software, die diese Container startet und verwaltet, heißt Docker.
In diesem Artikel installierst du Docker und Docker Compose, legst ein festes Verzeichnis für alle Anwendungen an und richtest zwei gemeinsame Netzwerke ein. Zum Schluss prüfst du mit einem kleinen Test, dass sich Container gegenseitig über ihren Namen finden.
Nebenbei legst du damit die Regeln fest, die ab jetzt für jede Anwendung gelten: wo ihre Dateien liegen, in welchem Netz sie hängt und welche Ports nach außen offen sind. Die wichtigste davon: Keine Anwendung bekommt einen eigenen Port am Server, denn jeder offene Port ist eine zusätzliche Angriffsfläche. Später gibt es genau einen Eingang – Caddy auf 80/443.
Am Ende hast du
- Docker Engine installiert und laufend (Debian 13)
- Docker Compose als Plugin installiert (
docker compose ...) - Eine saubere Basisstruktur für Stacks unter
/srv/docker - Zwei gemeinsame Netzwerke (
homelab_netundhomelab_intern) für alle Container - Log-Rotation aktiv – keine vollen Disks durch wuchernde Container-Logs
- 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
- Benutzer
martin(UID/GID 1000:1000) mit SSH-Schlüssel, eingerichtet in Artikel 02 - UFW aktiv, SSH offen
Getestet mit: Debian 13 · Docker Engine 29.9.3 · Docker Compose 5.7.0
Zuletzt durchgespielt: November 2026
1. Was ist Docker?
Ein Container ist ein fertig geschnürtes Softwarepaket: die Anwendung und alles, was sie zum Laufen braucht – Bibliotheken, Laufzeitumgebung, Grundeinstellungen. Der Name kommt vom Schiffscontainer, und der Vergleich trifft es: Egal was drin ist, verladen und gestartet wird er immer auf dieselbe Weise.
Für dich heißt das: Du installierst eine Anwendung nicht mehr auf dem Server, sondern startest ihr Paket. Auf dem Server selbst landet davon nichts. Entfernst du den Container, ist die Anwendung weg – ohne Reste.
Zwei Begriffe tauchen ab jetzt ständig auf:
- Abbild (Image) – die Vorlage, die Docker herunterlädt. Sie wird nie verändert.
- Container – ein laufendes Exemplar dieser Vorlage.
Was eine Anwendung speichert, liegt nicht im Container, sondern in einem Verzeichnis auf dem Server, das in den Container eingehängt wird. Deshalb kannst du einen Container jederzeit wegwerfen und neu erzeugen, ohne Daten zu verlieren. Genau so laufen später auch Updates: neues Abbild, neuer Container, dieselben Daten.
Docker Compose beschreibt in einer Textdatei, welche Container zu einer Anwendung gehören und wie sie verbunden sind. Statt langer Befehle genügt dann docker compose up.
Untereinander finden sich Container über ihren Namen, nicht über IP-Adressen. Du denkst also nicht in Zahlen, sondern in Namen wie vaultwarden, immich oder paperless.
2. Docker + Compose 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.
updateist nichtupgrade
sudo apt updateaktualisiert hier nur den Paket-Index – also die Liste, welche Pakete in welcher Version verfügbar sind. Es installiert oder aktualisiert nichts. Dein System selbst wurde bereits in Artikel 02 auf den aktuellen Stand gebracht und hält sich überunattended-upgradesautomatisch aktuell. Ein manuellesapt upgradeist hier also nicht nötig.
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:
Check
apt-cache policy docker-ce
Ausgabe: (Ausschnitt)
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 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 als Docker-Plugin (docker compose ...) |
2.5 Dienst prüfen
Docker startet den Dienst normalerweise automatisch. Wir prüfen jetzt, ob alles korrekt läuft.
Check
sudo systemctl status docker --no-pager
sudo docker run --rm hello-world
docker compose version
Ausgabe: active (running), danach die Begrüßung von hello-world mit „Hello from Docker!“ und zuletzt eine Versionsnummer von Docker Compose.
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. Zuerst die Verbindung beenden:
exit
Jetzt bist du wieder auf deinem eigenen Rechner. Von dort verbindest du dich neu – die beiden Befehle laufen also auf verschiedenen Geräten und gehören deshalb nicht in einen Rutsch:
ssh vps
Check
groups martin
docker ps
Ausgabe: In der Liste der Gruppen steht docker, und docker ps zeigt eine leere Tabelle mit Spaltenköpfen statt einer Fehlermeldung. Damit verwendest du Docker ab jetzt ohne sudo.
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"
Ausgabe: 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.
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.
Check
ls -la /srv/docker
df -h /srv/docker
Ausgabe: ls zeigt das Verzeichnis stacks mit martin martin als Eigentümer, df in der Spalte Avail den freien Platz auf der Systemdisk.
4. Die beiden Homelab-Netzwerke anlegen
Alle Container dieser Serie hängen in einem von zwei gemeinsamen Netzwerken. Der Unterschied ist eine einzige Eigenschaft: Das eine hat einen Weg ins Internet, das andere nicht.
docker network create homelab_net
docker network create --internal homelab_intern
Check
docker network ls | grep homelab
Ausgabe: (die ID in der ersten Spalte ist bei dir eine andere)
3c1d8e5a0b7f homelab_intern bridge local
9a4f2b6c1e0d homelab_net bridge local
Warum zwei Netze?
| Netz | Weg nach draußen | Wofür |
|---|---|---|
homelab_net |
ja | Anwendungen, die nachweislich ins Internet müssen |
homelab_intern |
nein | alles andere – und das ist die Mehrheit |
Der Grund ist nicht die Abwehr von außen; dafür sorgen Firewall und Caddy. Es geht um die Gegenrichtung: Ein Container-Abbild ist fremder Code, und ein Weg nach draußen ist der Weg, über den Daten hinausgehen können. Eine Notizanwendung, ein Dokumentenarchiv oder ein Passwort-Tresor braucht diesen Weg nicht – also bekommt er ihn auch nicht.
--internal ist dabei eine harte Sperre, keine Empfehlung: Der Container kommt schlicht nirgendwo hin.
Der Standard ist homelab_intern. In homelab_net kommt nur, wer nachweislich hinaus muss – der jeweilige App-Artikel begründet das an Ort und Stelle.
Woran du eine falsche Einordnung erkennst. Eine Anwendung im falschen Netz stürzt nicht ab, sie wird langsam: Die Namensauflösung läuft in eine Zeitüberschreitung, und es fehlt zum Beispiel ein Vorschaubild. Wenn eine App träge wirkt und Inhalte aus dem Netz nicht lädt, ist das der erste Verdacht.
Die dritte Ebene: ein eigenes Netz pro Stack
Anwendungen mit eigener Datenbank bekommen zusätzlich ein internes Netz nur für sich:
networks:
beispiel_internal:
name: beispiel_internal
internal: true
Darin liegen Anwendung und Datenbank. Im gemeinsamen Netz hängt nur der Webdienst – der, den Caddy erreichen muss. Damit ist die Datenbank für nichts erreichbar außer für ihre eigene Anwendung, und nicht für jeden anderen Container auf dem Server.
Dieses Netz legst du nicht von Hand an; es entsteht mit dem jeweiligen Stack. Wichtig ist nur die Zeile name: – ohne sie hängt Compose den Projektnamen davor, und docker network ls wird bei zwanzig Stacks unlesbar.
Caddy und der Mail-Weiterleiter sind die einzigen Container in beiden gemeinsamen Netzen – Caddy, weil er jede Anwendung erreichen muss, der Weiterleiter, damit auch Anwendungen ohne eigenen Ausgang Mail verschicken können.
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. Der Test läuft in homelab_intern – dem Netz, in dem später auch die meisten deiner Anwendungen liegen.
Test-Stack anlegen
mkdir -p /srv/docker/stacks/test
cat > /srv/docker/stacks/test/compose.yaml << 'EOF'
name: test
services:
whoami:
image: traefik/whoami
networks:
- homelab_intern
curl:
image: curlimages/curl:latest
command: ["sh","-c","curl -sS http://whoami:80 | grep -E 'Hostname|RemoteAddr'"]
networks:
- homelab_intern
depends_on:
- whoami
networks:
homelab_intern:
external: true
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_intern– das gemeinsame Netz ohne Weg nach draußen;external: trueheißt, Compose legt es nicht selbst an, sondern nutzt das bestehende
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).
Check
Ausgabe: (Hostname, Adresse und Portnummer sind bei dir andere)
Hostname: 1f8b2c6bd0eb
RemoteAddr: 172.19.0.3:54972
Das zeigt: curl hat whoami allein über den Service-Namen erreicht – ohne IP-Adresse. Die RemoteAddr-Zeile ist die interne Adresse in homelab_intern, von der die Anfrage kam. Die Zahl hinter dem Doppelpunkt ist der Absende-Port; den vergibt das System bei jeder Verbindung neu, er ist bei dir also ein anderer und bei jedem Durchlauf wieder. Die Namensauflösung funktioniert also auch in einem Netz ohne Weg nach draußen. Genau dieses Prinzip nutzt später Caddy.
Projekt-Status prüfen
docker compose ls
Ausgabe:
NAME STATUS CONFIG FILES
test running(1) /srv/docker/stacks/test/compose.yaml
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.yaml.
Aufräumen
docker compose down
cd ~
rm -rf /srv/docker/stacks/test
6. Die Namen in Compose
Bevor es ans Absichern geht, noch drei Begriffe, die dir in jeder compose.yaml der Reihe begegnen. Sie klingen ähnlich, meinen aber Verschiedenes.
Der Projekt-Name
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.yaml
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.
Projekt, Service, Container
| 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).
7. Docker und die Firewall
Wenn du in Compose ports: definierst, ist der Port sofort am Host erreichbar – auch wenn UFW aktiv ist.
Beispiel:
services:
paperless:
ports:
- "8000:8000" # ← Port 8000 ist jetzt öffentlich!
Viele gehen davon aus, dass UFW das blockiert. Das tut es nicht: Docker trägt seine Regeln für veröffentlichte Ports an einer Stelle ein, die der Verkehr passiert, bevor die Regeln von UFW zum Zug kommen. In sudo ufw status taucht der Port nicht auf – offen ist er trotzdem. Auch die vorgelagerte Firewall aus Artikel 02 fängt das nicht auf: Sie lässt eingehend ab Werk alles durch.
Unsere Lösung: Nur ein öffentlicher Einstiegspunkt
Regel für diese Serie:
- Anwendungen veröffentlichen keine Ports am Server –
ports:kommt in ihren Compose-Dateien nicht vor - Öffentlich erreichbar ist später nur Caddy, auf 80 und 443
- Caddy erreicht die Anwendungen von innen, über die beiden gemeinsamen Netze
Vorteil:
| Ohne Reverse Proxy | Mit Reverse Proxy (Caddy) |
|---|---|
| Immich: Port 2283 | fotos.deine-domain.de → Caddy → immich:2283 |
| Paperless: Port 8000 | docs.deine-domain.de → Caddy → paperless:8000 |
| Forgejo: Port 3000 | git.deine-domain.de → Caddy → forgejo:3000 |
| Offen: 2283, 8000, 3000 – einer je Anwendung | Offen: 80, 443 – egal wie viele Anwendungen |
Außen nur ein Eingang, innen beliebig viele Dienste. Die einzige Anwendung mit einem eigenen Port ist Komodo in Artikel 06 – und der liegt nicht öffentlich, sondern allein auf der VPN-Adresse des Servers.
8. Best Practices für Docker auf VPS
- Sprechende Service-Namen –
vaultwarden, nichtapp-v2 - Explizite Projekt-Namen –
name:ganz oben incompose.yaml - Nur ein öffentlicher Einstiegspunkt – Caddy auf 80/443, kein anderer Stack nutzt
ports: - Routing über Domains –
cloud.deine-domain.de, nicht:8080 - Zwei gemeinsame Netzwerke –
homelab_netundhomelab_intern - 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.
Wenn es nicht klappt
| Was du siehst | Ursache | Weiter |
|---|---|---|
permission denied while trying to connect to the Docker daemon socket |
Die Gruppe docker wirkt erst nach einer neuen Anmeldung |
→ Abschnitt 2.6: exit, dann ssh vps |
Nach exit läuft ssh vps nicht mehr mit |
Beide Befehle am Stück eingefügt – ssh vps gehört auf deinen Rechner, nicht auf den Server |
→ Abschnitt 2.6, zwei getrennte Blöcke |
| Eine Anwendung lädt träge, Vorschaubilder fehlen | Sie hängt in homelab_intern, braucht aber einen Weg nach draußen |
→ Abschnitt 4, Kasten |
Und jetzt?
Du hast jetzt eine stabile Docker-Plattform auf deinem VPS:
- Docker Engine läuft
- Docker Compose ist einsatzbereit
- Log-Rotation ist aktiv – keine vollen Disks durch wuchernde Container-Logs
homelab_netundhomelab_internverbinden 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 Artikel verbindest du deine Domain mit dem Server: Über die DNS-Einträge kommen Aufrufe von deine-domain.de und ihren Subdomains bei deinem VPS an. Das ist die Voraussetzung dafür, dass Caddy später für jede Anwendung ein Zertifikat bekommt.
👉 Nächster Artikel: 04. Domain und DNS auf dem VPS: Adressen, die auf deinen Server zeigen
Discussion