💡 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 -ySchritt 2: Abhängigkeiten installieren
sudo apt update
sudo apt install -y ca-certificates curl gnupg lsb-releaseSchritt 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.ascSchritt 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/nullHinweis 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-pluginSchritt 6: Installation prüfen
sudo docker version
sudo docker run hello-worldWenn 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 dockerDocker 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_benutzernameWichtig: Du musst dich ab- und wieder anmelden, damit die Gruppenmitgliedschaft aktiv wird:
sudo newgrp dockerTest ohne sudo:
docker psdocker-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 nginxContainer 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-nginxLogs 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-nginxIn 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.confEigenes 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-appDatei 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
.gitignoreImage 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:3000Du 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 versiondocker-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 NetzwerkeWichtige Optionen erklärt
| Option | Bedeutung |
|---|---|
image | Docker-Image, das verwendet wird |
build | Pfad zum Dockerfile (statt fertigem Image) |
ports | Port-Weiterleitungen (Host:Container) |
volumes | Daten-Mounts (persistent!) |
environment | Umgebungsvariablen |
env_file | Variablen aus .env-Datei laden |
restart | no, always, unless-stopped, on-failure |
depends_on | Service-Abhängigkeiten (Startreihenfolge) |
networks | Netzwerk-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-dockerErstelle 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: bridgeStarten 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/_dataVorteile: 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/dataVorteile: Direkter Zugriff auf die Dateien vom Host aus. Ideal für Konfigurationsdateien und Entwicklung.
3. tmpfs Mounts
# Nur im RAM — verschwindet beim Neustart
tmpfs:
- /tmpVorteile: 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_dataBackup 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 /targetNetzwerke 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 app1Warum 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 pruneDocker 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 dfGezielt 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 pruneAlles auf einmal
# Der "Staubsauger" — entfernt alles Unbenutzte
docker system prune
# Inklusive Volumes (⚠️ Vorsicht!)
docker system prune -a --volumesdocker 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 appuserOder zur Laufzeit:
docker run --user 1000:1000 nginx2. 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 \
nginxIn Docker Compose:
services:
app:
image: nginx
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M3. Read-Only Filesystem
docker run --read-only --tmpfs /tmp nginx4. 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 nginx5. 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:latest6. 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 erreichbarZusammenfassung & Checkliste
Du hast jetzt ein solides Docker-Setup auf Debian 13! Hier eine Checkliste zum Abhaken:
☐ 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 dockerProblem: „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 dockerProblem: 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 nginxProblem: Kein Speicherplatz mehr
# Docker-Speicherverbrauch anzeigen
docker system df
# Aufräumen
docker system prune -aProblem: 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 nginxProblem: 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!
Schreibe einen Kommentar