*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.

Neu in der Reihe? Hier geht's los
Hardware & Setup dieser Serie

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_net und homelab_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.

update ist nicht upgrade

sudo apt update aktualisiert 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 über unattended-upgrades automatisch aktuell. Ein manuelles apt upgrade ist 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 – fragt http://whoami:80 ab (über Service-Namen!) und filtert die zwei aussagekräftigen Zeilen heraus
  • Netzwerk homelab_intern – das gemeinsame Netz ohne Weg nach draußen; external: true heiß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 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).

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:

  1. Automatisch – Compose nutzt den Verzeichnisnamen (test aus /srv/docker/stacks/test)
  2. Explizit – Du setzt name: ganz oben in compose.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:80 an (nicht http://vw-prod:80, selbst wenn du container_name: vw-prod gesetzt 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, nicht app-v2
  • Explizite Projekt-Namen – name: ganz oben in compose.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_net und homelab_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 expliziten docker 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_net und homelab_intern verbinden 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