> ## Content Index
> Fetch the complete content index at: https://blog.cube-m.de/llms.txt
> Use this file to discover other available public pages before exploring further.

# 03. Docker als Plattform: Engine + Compose v2 auf Debian 13 (VPS-Setup)
- URL: https://blog.cube-m.de/docker-als-plattform-engine-compose-v2-auf-debian-13-vps-setup/
- Published: 2026-01-11T16:22:54.000Z
- Updated: 2026-06-24T12:56:36.000Z
- Description: Docker als Plattform aufsetzen: Engine, Compose v2, Log-Rotation und ein gemeinsames internes Netzwerk für alle späteren Apps. Mit dem Prinzip: außen ein Eingang, innen beliebig viele Dienste.
- Author: Martin
- Tags: VPS, Basis, docker, self-hosting, Server, #Import 2026-09-06 19:52

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

```bash
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.

```bash
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:

```bash
apt-cache policy docker-ce

```

*Erwartung*:

```text
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**

```bash
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.

```bash
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.

```bash
sudo usermod -aG docker martin

```

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

```bash
exit
ssh martin@<SERVER-IP>

```

Danach prüfen:

```bash
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:

```bash
sudo nano /etc/docker/daemon.json

```

Inhalt einfügen:

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

```

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

```bash
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**

```bash
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**

```bash
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**

```bash
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`.

```bash
docker network create homelab_net

```

**Prüfen**

```bash
docker network ls | grep homelab

```

*Erwartung*:

```text
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**

```bash
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:

```bash
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**

```bash
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**

```bash
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:

```yaml
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:

```yaml
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:

```text
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-Namen** – `vaultwarden`, nicht `app-v2`
- **Explizite Projekt-Namen** – `name:` ganz oben in `compose.yml`
- **Nur ein öffentlicher Einstiegspunkt** – Caddy auf 80/443
- **Routing über Domains** – `cloud.example.de`, nicht `:8080`
- **Ein gemeinsames Netzwerk** – `homelab_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](#)