💡 Hinweis: Dieses Tutorial setzt voraus, dass du einen normalen Benutzer mit sudo-Rechten verwendest — wie im Tutorial Debian 13 Server absichern beschrieben.

Einleitung — Was ist Docker? Warum Container?

Docker hat die Art und Weise, wie wir Software entwickeln, testen und bereitstellen, grundlegend verändert. Statt Anwendungen direkt auf dem Host-System zu installieren — mit all den Abhängigkeiten, Konflikten und dem berüchtigten „Bei mir läuft’s aber!“ — verpackt Docker alles in sogenannte Container.

Ein Container ist eine isolierte, leichtgewichtige Umgebung, die alles mitbringt, was eine Anwendung zum Laufen braucht: Code, Laufzeitumgebung, Bibliotheken und Systemtools. Im Gegensatz zu virtuellen Maschinen teilen sich Container den Kernel des Host-Systems und starten in Sekundenbruchteilen.

Warum Container?

  • Reproduzierbarkeit: Was auf deinem Rechner läuft, läuft auch auf dem Server — garantiert.
  • Isolation: Jede Anwendung hat ihre eigene Umgebung. Keine Konflikte zwischen PHP 7.4 und PHP 8.3.
  • Portabilität: Ein Container läuft auf jedem System mit Docker — egal ob Debian, Ubuntu oder CentOS.
  • Skalierbarkeit: Brauchst du mehr Leistung? Starte einfach weitere Container.
  • Versionierung: Container-Images lassen sich taggen, versionieren und zurückrollen.

In diesem Tutorial installieren wir Docker und Docker Compose auf Debian 13 (Trixie) und arbeiten uns von den Grundlagen bis zu einem produktionsreifen Setup durch.


Voraussetzungen

Bevor wir loslegen, brauchst du:

  • Einen Debian 13 (Trixie) Server mit Root-Zugang oder einem Benutzer mit sudo-Rechten
  • Einen abgesicherten Server — wenn du das noch nicht gemacht hast, folge zuerst unserem Tutorial #1: Debian 13 Server absichern
  • Eine stabile Internetverbindung (für das Herunterladen von Docker und Images)
  • Grundlegende Linux-Kenntnisse (Terminal, Dateien bearbeiten, Pakete installieren)

Alle Befehle in diesem Tutorial werden als root oder mit sudo ausgeführt. Wenn du als normaler Benutzer arbeitest, stelle jedem Befehl sudo voran.


Docker installieren (Official Docker Repository)

Warum NICHT das Debian-Paket?

Debian liefert Docker unter dem Paketnamen docker.io aus. Klingt praktisch, hat aber entscheidende Nachteile:

  • Veraltete Version: Das Debian-Paket hinkt oft Monate oder sogar Major-Versionen hinter dem offiziellen Release her.
  • Fehlende Features: Neue Docker-Funktionen (BuildKit-Verbesserungen, Compose v2 als Plugin) sind im Debian-Paket oft nicht enthalten.
  • Kein offizieller Support: Dockerʼs Dokumentation und Troubleshooting beziehen sich auf die offizielle Installation.

Wir nutzen daher das offizielle Docker-Repository — immer aktuell, immer kompatibel.

Schritt 1: Alte Pakete entfernen

Falls bereits eine ältere Docker-Version installiert ist:

sudo apt remove docker docker-engine docker.io containerd runc 2>/dev/null
sudo apt autoremove -y

Schritt 2: Abhängigkeiten installieren

sudo apt update
sudo apt install -y ca-certificates curl gnupg lsb-release

Schritt 3: Docker GPG-Schlüssel hinzufügen

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

Schritt 4: Docker-Repository einrichten

sudo echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
  $(. /etc/os-release && sudo echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Hinweis für Debian 13 (Trixie): Falls Docker das Repository für Trixie noch nicht offiziell anbietet, kannst du $VERSION_CODENAME durch bookworm ersetzen. Die Pakete sind binärkompatibel.

Schritt 5: Docker installieren

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

Schritt 6: Installation prüfen

sudo docker version
sudo docker run hello-world

Wenn du die „Hello from Docker!“-Nachricht siehst, läuft alles korrekt. 🎉

Docker-Dienst aktivieren

sudo systemctl enable docker
sudo systemctl start docker
sudo systemctl status docker

Docker ohne sudo nutzen

Standardmäßig benötigt Docker Root-Rechte. Das ist im täglichen Gebrauch unpraktisch. Die Lösung: Füge deinen Benutzer zur docker-Gruppe hinzu.

sudo usermod -aG docker dein_benutzername

Wichtig: Du musst dich ab- und wieder anmelden, damit die Gruppenmitgliedschaft aktiv wird:

sudo newgrp docker

Test ohne sudo:

docker ps
⚠️ Sicherheitshinweis: Jeder Benutzer in der docker-Gruppe hat effektiv Root-Zugang zum Host-System! Füge nur vertrauenswürdige Benutzer hinzu. Mehr dazu im Abschnitt „Sicherheit“.

Erste Schritte mit Docker

Container starten: docker run

Der grundlegendste Docker-Befehl:

# Einen Nginx-Webserver starten
docker run -d --name mein-nginx -p 8080:80 nginx

# Was passiert hier?
# -d          → Container läuft im Hintergrund (detached)
# --name      → Gibt dem Container einen Namen
# -p 8080:80  → Leitet Port 8080 des Hosts auf Port 80 im Container
# nginx       → Das Docker-Image

Öffne http://deine-server-ip:8080 im Browser — du siehst die Nginx-Willkommensseite.

Images verwalten

# Alle lokal vorhandenen Images anzeigen
docker images

# Ein Image herunterladen (ohne Container zu starten)
docker pull ubuntu:24.04

# Ein Image löschen
docker rmi nginx

Container verwalten

# Laufende Container anzeigen
docker ps

# ALLE Container anzeigen (auch gestoppte)
docker ps -a

# Container stoppen
docker stop mein-nginx

# Container starten
docker start mein-nginx

# Container löschen (muss gestoppt sein)
docker rm mein-nginx

# Container löschen (auch wenn er läuft)
docker rm -f mein-nginx

Logs anzeigen

# Alle Logs eines Containers
docker logs mein-nginx

# Logs live verfolgen (wie tail -f)
docker logs -f mein-nginx

# Nur die letzten 50 Zeilen
docker logs --tail 50 mein-nginx

In einen Container hineinschauen

# Shell in einem laufenden Container öffnen
docker exec -it mein-nginx bash

# Einen einzelnen Befehl ausführen
docker exec mein-nginx cat /etc/nginx/nginx.conf

Eigenes Dockerfile schreiben

Ein Dockerfile ist eine Textdatei mit Anweisungen, wie ein Docker-Image gebaut wird. Hier erstellen wir eine einfache Node.js-Anwendung.

Die Anwendung

Erstelle einen Ordner und die nötigen Dateien:

mkdir ~/meine-app && cd ~/meine-app

Datei package.json:

{
  "name": "meine-docker-app",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.21.0"
  }
}

Datei server.js:

const express = require('express');
const app = express();
const PORT = 3000;

app.get('/', (req, res) => {
  res.json({
    message: 'Hallo aus Docker! 🐳',
    hostname: require('os').hostname(),
    timestamp: new Date().toISOString()
  });
});

app.listen(PORT, () => {
  console.log(`Server läuft auf Port ${PORT}`);
});

Das Dockerfile

# Basis-Image: Node.js 22 auf Alpine (klein & sicher)
FROM node:22-alpine

# Arbeitsverzeichnis im Container
WORKDIR /app

# Erst package.json kopieren (für besseres Caching)
COPY package.json ./

# Abhängigkeiten installieren
RUN npm install --production

# Restlichen Quellcode kopieren
COPY . .

# Port dokumentieren (informativ, öffnet nichts)
EXPOSE 3000

# Nicht als Root laufen!
USER node

# Startbefehl
CMD ["npm", "start"]

Eine .dockerignore-Datei verhindert, dass unnötige Dateien ins Image gelangen:

node_modules
npm-debug.log
.git
.gitignore

Image bauen und starten

# Image bauen
docker build -t meine-app:1.0 .

# Container starten
docker run -d --name meine-app -p 3000:3000 meine-app:1.0

# Testen
curl http://localhost:3000

Du solltest eine JSON-Antwort mit „Hallo aus Docker!“ sehen.


Docker Compose installieren (v2, Plugin)

Wenn du Docker wie oben beschrieben installiert hast, ist Docker Compose v2 bereits dabei — es wurde als docker-compose-plugin mitinstalliert.

Prüfe die Version:

docker compose version
ℹ️ Compose v1 vs. v2: Das alte docker-compose (mit Bindestrich) ist veraltet und wird nicht mehr gepflegt. Nutze immer docker compose (ohne Bindestrich) — das ist die v2, die als Docker-Plugin läuft und deutlich schneller ist.

docker-compose.yml verstehen & schreiben

Eine docker-compose.yml (oder compose.yml) beschreibt deine gesamte Anwendung: Welche Container (Services) laufen, wie sie miteinander verbunden sind, welche Ports offen sind und wo Daten gespeichert werden.

Grundstruktur

services:
  service-name:
    image: image-name:tag        # Welches Image?
    ports:
      - "host:container"         # Port-Mapping
    environment:
      - VARIABLE=wert            # Umgebungsvariablen
    volumes:
      - ./lokal:/im/container    # Daten-Mapping
    restart: unless-stopped      # Neustart-Verhalten
    depends_on:
      - anderer-service          # Startreihenfolge

volumes:
  mein-volume:                   # Benannte Volumes

networks:
  mein-netzwerk:                 # Eigene Netzwerke

Wichtige Optionen erklärt

OptionBedeutung
imageDocker-Image, das verwendet wird
buildPfad zum Dockerfile (statt fertigem Image)
portsPort-Weiterleitungen (Host:Container)
volumesDaten-Mounts (persistent!)
environmentUmgebungsvariablen
env_fileVariablen aus .env-Datei laden
restartno, always, unless-stopped, on-failure
depends_onService-Abhängigkeiten (Startreihenfolge)
networksNetzwerk-Zuordnung

Praxis-Beispiel: WordPress + MariaDB mit Docker Compose

Jetzt wird es praktisch! Wir setzen eine komplette WordPress-Installation mit MariaDB-Datenbank auf — in wenigen Minuten.

mkdir ~/wordpress-docker && cd ~/wordpress-docker

Erstelle eine .env-Datei für sensible Daten (niemals Passwörter direkt in die compose.yml!):

# .env
MYSQL_ROOT_PASSWORD=superGeheimesRootPasswort123!
MYSQL_DATABASE=wordpress
MYSQL_USER=wp_user
MYSQL_PASSWORD=sicheresWpPasswort456!

Erstelle die compose.yml:

services:
  db:
    image: mariadb:11
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - wp-network
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 5

  wordpress:
    image: wordpress:6-php8.3-apache
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_NAME: ${MYSQL_DATABASE}
      WORDPRESS_DB_USER: ${MYSQL_USER}
      WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - wp_data:/var/www/html
    networks:
      - wp-network

volumes:
  db_data:
  wp_data:

networks:
  wp-network:
    driver: bridge

Starten und verwalten

# Alles starten (im Hintergrund)
docker compose up -d

# Status prüfen
docker compose ps

# Logs anzeigen
docker compose logs -f

# Nur WordPress-Logs
docker compose logs -f wordpress

# Alles stoppen
docker compose down

# Stoppen UND Volumes löschen (⚠️ Datenverlust!)
docker compose down -v

Öffne http://deine-server-ip:8080 — WordPress begrüßt dich mit dem Installationsassistenten!


Volumes & Persistenz — Daten nicht verlieren!

Container sind flüchtig (ephemeral). Wenn du einen Container löschst, sind alle Daten darin weg. Volumes lösen dieses Problem.

Drei Arten von Mounts

1. Named Volumes (empfohlen)

# In compose.yml
volumes:
  - db_data:/var/lib/mysql

# Docker verwaltet den Speicherort
# Typisch: /var/lib/docker/volumes/db_data/_data

Vorteile: Docker verwaltet alles, funktioniert auf jedem System, einfach zu sichern.

2. Bind Mounts

# Lokales Verzeichnis direkt einbinden
volumes:
  - ./meine-config:/etc/nginx/conf.d
  - /home/user/daten:/app/data

Vorteile: Direkter Zugriff auf die Dateien vom Host aus. Ideal für Konfigurationsdateien und Entwicklung.

3. tmpfs Mounts

# Nur im RAM — verschwindet beim Neustart
tmpfs:
  - /tmp

Vorteile: Schnell, keine Daten auf der Festplatte (gut für sensible temporäre Daten).

Volumes verwalten

# Alle Volumes anzeigen
docker volume ls

# Details zu einem Volume
docker volume inspect db_data

# Unbenutzte Volumes löschen
docker volume prune

# Bestimmtes Volume löschen
docker volume rm db_data

Backup eines Volumes

# Volume in eine tar-Datei sichern
docker run --rm \
  -v db_data:/source:ro \
  -v $(pwd):/backup \
  alpine tar czf /backup/db_backup_$(date +%Y%m%d).tar.gz -C /source .

# Wiederherstellen
docker run --rm \
  -v db_data:/target \
  -v $(pwd):/backup \
  alpine tar xzf /backup/db_backup_20260217.tar.gz -C /target

Netzwerke in Docker

Docker erstellt automatisch Netzwerke, damit Container miteinander kommunizieren können.

Standard-Netzwerktypen

  • bridge (Standard): Isoliertes Netzwerk für Container auf einem Host. Container können sich über ihren Namen erreichen.
  • host: Container nutzt direkt das Netzwerk des Hosts — kein Port-Mapping nötig, aber keine Isolation.
  • none: Kein Netzwerk. Komplett isoliert.

Eigene Netzwerke erstellen

# Netzwerk erstellen
docker network create mein-netzwerk

# Container in ein Netzwerk starten
docker run -d --name app1 --network mein-netzwerk nginx
docker run -d --name app2 --network mein-netzwerk alpine sleep 3600

# app2 kann app1 über den Namen erreichen:
docker exec app2 ping app1

Warum eigene Netzwerke?

  • DNS-Auflösung: Container können sich gegenseitig über ihren Namen finden (statt IP-Adressen).
  • Isolation: Nur Container im selben Netzwerk können miteinander sprechen.
  • Sicherheit: Die Datenbank ist nur für die App erreichbar, nicht von außen.

In Docker Compose wird automatisch ein Netzwerk für alle Services erstellt. Du kannst aber auch eigene definieren, um Services voneinander zu trennen:

services:
  frontend:
    networks:
      - frontend-net
      - backend-net
  api:
    networks:
      - backend-net
      - db-net
  database:
    networks:
      - db-net

networks:
  frontend-net:
  backend-net:
  db-net:

Hier kann frontend nicht direkt auf database zugreifen — nur über api.

Netzwerke verwalten

# Alle Netzwerke anzeigen
docker network ls

# Details und verbundene Container
docker network inspect mein-netzwerk

# Unbenutzte Netzwerke entfernen
docker network prune

Docker aufräumen

Docker sammelt mit der Zeit viel „Müll“ an: gestoppte Container, ungenutzte Images, verwaiste Volumes. Das kann schnell Gigabytes verschlingen.

Speicherverbrauch anzeigen

docker system df

Gezielt aufräumen

# Gestoppte Container entfernen
docker container prune

# Unbenutzte Images entfernen (nur "dangling")
docker image prune

# ALLE unbenutzten Images entfernen (auch getaggte!)
docker image prune -a

# Verwaiste Volumes entfernen
docker volume prune

# Unbenutzte Netzwerke entfernen
docker network prune

Alles auf einmal

# Der "Staubsauger" — entfernt alles Unbenutzte
docker system prune

# Inklusive Volumes (⚠️ Vorsicht!)
docker system prune -a --volumes
🚨 Vorsicht: docker system prune -a --volumes löscht ALLE ungenutzten Images und Volumes — auch solche, die du vielleicht noch brauchst. Nutze diesen Befehl mit Bedacht!

Automatisches Aufräumen per Cronjob

# Wöchentlich alte Images und Container aufräumen
echo "0 3 * * 0 docker system prune -f --filter 'until=168h'" | crontab -

Dieser Cronjob entfernt jeden Sonntag um 3 Uhr nachts alles, was älter als 7 Tage ist.


Sicherheit: Docker absichern

Docker ist mächtig — und genau deshalb ein Sicherheitsrisiko, wenn man es falsch konfiguriert.

1. Kein Root in Containern!

Standardmäßig laufen Prozesse in Containern als root. Das ist gefährlich, weil ein Container-Ausbruch dem Angreifer Root-Rechte auf dem Host geben kann.

# Im Dockerfile: Eigenen Benutzer verwenden
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

Oder zur Laufzeit:

docker run --user 1000:1000 nginx

2. Resource Limits setzen

Verhindere, dass ein Container den ganzen Server lahmlegt:

# Maximal 512 MB RAM und 1 CPU-Kern
docker run -d \
  --memory=512m \
  --cpus=1.0 \
  --name begrenzt \
  nginx

In Docker Compose:

services:
  app:
    image: nginx
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 128M

3. Read-Only Filesystem

docker run --read-only --tmpfs /tmp nginx

4. Keine privilegierten Container

# NIEMALS in Produktion:
docker run --privileged nginx  # ❌ Voller Host-Zugriff!

# Stattdessen: Nur benötigte Capabilities
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx

5. Images aktuell halten

# Regelmäßig neue Image-Versionen pullen
docker compose pull
docker compose up -d

# Images auf Schwachstellen prüfen
docker scout cves nginx:latest

6. Docker-Socket schützen

Der Docker-Socket (/var/run/docker.sock) ist der Schlüssel zum Königreich. Wer darauf Zugriff hat, kontrolliert den gesamten Host. Mounte ihn niemals in Container, denen du nicht zu 100% vertraust.

7. Docker-Netzwerk und Firewall

Docker manipuliert iptables direkt. Das bedeutet: UFW-Regeln werden von Docker umgangen! Container-Ports sind trotz UFW von außen erreichbar.

Lösung: Binde Container nur an localhost und nutze einen Reverse Proxy:

# Statt:
ports:
  - "8080:80"     # Von überall erreichbar!

# Besser:
ports:
  - "127.0.0.1:8080:80"  # Nur lokal erreichbar

Zusammenfassung & Checkliste

Du hast jetzt ein solides Docker-Setup auf Debian 13! Hier eine Checkliste zum Abhaken:

✅ Docker-Checkliste

☐ Docker aus dem offiziellen Repository installiert
☐ Docker-Dienst läuft und ist aktiviert (sudo systemctl enable docker)
☐ Benutzer zur docker-Gruppe hinzugefügt
☐ Docker Compose v2 funktioniert (docker compose version)
☐ Erste Container getestet
☐ Volumes für persistente Daten konfiguriert
☐ Eigene Netzwerke für Service-Isolation eingerichtet
☐ Container laufen NICHT als Root
☐ Resource Limits gesetzt
☐ Ports nur an 127.0.0.1 gebunden (mit Reverse Proxy)
☐ Aufräum-Strategie etabliert (prune-Cronjob)
☐ Backup-Strategie für Volumes vorhanden


Troubleshooting

Problem: „permission denied while trying to connect to the Docker daemon socket“

Ursache: Dein Benutzer ist nicht in der docker-Gruppe.

sudo usermod -aG docker $USER
# Dann ab- und wieder anmelden!
newgrp docker

Problem: „Cannot connect to the Docker daemon“

Ursache: Docker läuft nicht.

sudo systemctl start docker
sudo systemctl status docker
# Logs prüfen:
sudo journalctl -xu docker

Problem: Container startet, ist aber sofort wieder gestoppt

Ursache: Die Anwendung im Container crasht.

# Logs des Containers prüfen
docker logs container-name

# Exit-Code anzeigen
docker inspect container-name --format='{{.State.ExitCode}}'

Problem: „port is already allocated“

Ursache: Ein anderer Dienst oder Container nutzt bereits den Port.

# Welcher Prozess nutzt den Port?
sudo ss -tlnp | grep :8080

# Oder einen anderen Port verwenden:
docker run -p 8081:80 nginx

Problem: Kein Speicherplatz mehr

# Docker-Speicherverbrauch anzeigen
docker system df

# Aufräumen
docker system prune -a

Problem: Container können sich nicht über Namen erreichen

Ursache: Container sind nicht im selben benutzerdefinierten Netzwerk.

# DNS-Auflösung funktioniert nur in eigenen Netzwerken,
# NICHT im Standard-Bridge-Netzwerk!
docker network create mein-netz
docker run -d --name app --network mein-netz nginx

Problem: Docker umgeht die Firewall (UFW)

# Ports an localhost binden:
ports:
  - "127.0.0.1:8080:80"

# Oder Docker iptables-Manipulation deaktivieren:
# /etc/docker/daemon.json
{
  "iptables": false
}

Hinweis: Bei "iptables": false musst du das Netzwerk-Routing selbst verwalten.


Nächste Schritte

Docker läuft, Container sind aufgesetzt — aber wie machst du deine Services sicher über HTTPS erreichbar? Dafür brauchst du einen Reverse Proxy.

Im nächsten Tutorial zeigen wir dir, wie du mit Nginx Proxy Manager oder Traefik als Docker-Container einen Reverse Proxy einrichtest, der:

  • Automatisch Let’s Encrypt SSL-Zertifikate holt und erneuert
  • Mehrere Domains auf verschiedene Container weiterleitet
  • Als zentrale Anlaufstelle für allen eingehenden Traffic dient

👉 Weiter zu Tutorial #6: Reverse Proxy mit Docker (kommt bald)


Dieses Tutorial ist Teil unserer Serie „Debian 13 Server von Null auf Produktionsreif“. Hast du Fragen oder Probleme? Schreib es in die Kommentare!