SSD Nodes Learn
Anleitungen Matt ConnorVon Matt Connor · Aktualisiert 2026-07-24

Docker Compose auf Ubuntu 24.04 installieren

Lernen Sie die Installation von Docker Engine und Compose v2 auf Ubuntu 24.04. Vermeiden Sie UFW-Fehler bei der Port-Veröffentlichung und sichern Sie Volumes.

Was Sie aufbauen

Docker Compose bildet die Grundlage für fast alle anderen Dienste auf dieser Seite. Nextcloud, Vaultwarden, n8n, Immich, Rocket.Chat — jede dieser Anleitungen beginnt mit der Anweisung „schreiben Sie diese Compose-Datei“. Diese Seite erklärt die Bedeutung dieser Datei. Sie installieren die Docker Engine und das Compose v2 Plugin über das offizielle apt-Repository von Docker auf Ubuntu 24.04. Danach richten Sie einen Stack mit zwei Diensten ein: Miniflux, einen kleinen RSS-Reader, und PostgreSQL. Diese Kombination nutzt alle Muster, die auch größere Anwendungen verwenden: feste Image-Versionen, eine Datenbank mit Healthcheck, ein benannter Volume, Secrets in einer .env Datei und ein Port, der nur für localhost freigegeben ist.

Die Installation dauert fünf Minuten. Der Rest dieser Anleitung behandelt Probleme, die später auftreten können: die Docker-Gruppe, die faktisch Root-Rechte besitzt, veröffentlichte Ports, die UFW-Regeln umgehen, und ein Flag bei docker compose down, das die Datenbank ohne Bestätigung löscht.

Voraussetzungen: ein frisches Ubuntu 24.04 KVM VPS, ein Benutzer mit sudo-Rechten und mindestens ein Gigabyte RAM. Eine bereits installierte Docker-Version ist möglich — der erste Abschnitt beschreibt die notwendigen Entfernungsschritte.

Nicht aus dem Ubuntu-Repo, sondern aus dem Docker-Repo installieren

Vor dem ersten Befehl müssen zwei Fehler vermieden werden. Das Standard-docker.io-Paket von Ubuntu funktioniert zwar, ist aber versionsmäßig hinter den Docker-Releases und nutzt eine andere Plugin-Struktur. Die eigenständige docker-compose-Binary (mit Bindestrich) ist Compose v1: Sie basiert auf Python, ist seit 2023 am Ende ihres Lebenszyklus (end-of-life) und führt dazu, dass ältere Tutorials fehlschlagen. Das aktuelle Compose ist docker compose (mit Leerzeichen), ein CLI-Plugin, das aus demselben Repository wie die Engine installiert wird.

Falls diese Komponenten bereits auf dem System installiert sind, entfernen Sie diese zuerst. Dies schließt docker-compose-v2 ein, die Ubuntu-eigene Paketierung des Plugins, damit alle Komponenten aus einem einzigen Repository stammen:

sudo apt remove -y docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc

Package 'docker.io' is not installed, so not removed ist die Standardausgabe auf einem frischen VPS. Fügen Sie dann das Docker-Repository hinzu und installieren Sie die Pakete:

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/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Überprüfen Sie alle drei Ebenen:

docker --version
docker compose version
sudo docker run --rm hello-world

Die ersten beiden Befehle geben Versionsstrings aus — Docker Compose version v2.x.x bestätigt, dass das Plugin vorhanden ist und nicht die veraltete v1-Binary. Der Befehl hello-world sollte mit Hello from Docker! abschließen. Das Paket aktiviert den Dienst beim Systemstart; systemctl is-enabled docker gibt enabled aus.

Die docker-Gruppe gehört root — treffen Sie eine bewusste Entscheidung

Aktuell benötigt jeder docker-Befehl sudo, da das Daemon-Socket unter /var/run/docker.sock root und der docker-Gruppe gehört. Ohne die entsprechende Gruppenmitgliedschaft erhalten Sie den am häufigsten gesuchten Docker-Fehler:

permission denied while trying to connect to the Docker daemon socket at
unix:///var/run/docker.sock

Die Standardlösung:

sudo usermod -aG docker $USER

Die Gruppenmitgliedschaft wird erst beim Login wirksam. Der Fehler bleibt in der aktuellen Shell bestehen. Führen Sie newgrp docker für diese Sitzung aus oder melden Sie sich ab und wieder an; id sollte docker dann in Ihren Gruppen auflisten.

Der entscheidende Punkt: Die Mitgliedschaft in der docker-Gruppe entspricht root auf dem Host. Nicht "ähnlich wie root" oder "erhöhte Rechte" — sondern root. Jeder Benutzer in dieser Gruppe kann docker run --rm -it -v /:/host alpine chroot /host ausführen und das gesamte Dateisystem kontrollieren, ohne ein Passwort einzugeben. Die Gruppe dient der Bequemlichkeit, nicht der Isolation.

Der Rootless-Modus von Docker ist die echte Alternative — dabei läuft das Daemon selbst als Ihr unprivilegierter Benutzer. Dies hat folgende Nachteile: Ports unter 1024 erfordern eine zusätzliche Konfiguration, das Networking läuft über einen Userspace-Shim mit messbarem Overhead, und einige Images funktionieren ohne echte Root-Rechte nicht korrekt. Auf einem VPS mit nur einem Administrator, der bereits sudo nutzt, ändert die Gruppe an der Praxis nichts. Alle Anleitungen hier gehen von diesem Setup aus — geben Sie die Rechte jedoch niemals so weiter, als wären sie weniger mächtig als sudo.

Anatomie einer Compose-Datei

Geben Sie jedem Stack ein eigenes Verzeichnis — der Verzeichnisname wird zum Projektnamen, der Container, Netzwerke und Volumes voranstellt:

sudo mkdir -p /opt/miniflux && sudo chown $USER /opt/miniflux && cd /opt/miniflux

Erstellen Sie compose.yml (der moderne Name; docker-compose.yml funktioniert noch). Überspringen Sie den alten version:-Key — dieser ist veraltet und Compose warnt bei dessen Verwendung.

services:
  miniflux:
    image: miniflux/miniflux:2.2.9
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:8080"
    environment:
      - DATABASE_URL=postgres://miniflux:${POSTGRES_PASSWORD}@db/miniflux?sslmode=disable
      - RUN_MIGRATIONS=1
      - CREATE_ADMIN=1
      - ADMIN_USERNAME=admin
      - ADMIN_PASSWORD=${ADMIN_PASSWORD}
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      - POSTGRES_USER=miniflux
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=miniflux
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "miniflux", "-d", "miniflux"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  db-data:

Jede Zeile oben ist eine Entscheidung. Gehen Sie diese nacheinander durch.

Image-Versionen fixieren — :latest plus ein Pull ist ein unkontrolliertes Upgrade

postgres:16-alpine, nicht postgres:latest. Ein Tag ist nicht statisch: :latest löst bei jedem Pull erneut auf, was der Maintainer zuletzt gepusht hat. Kombinieren Sie dies mit der Routine für regelmäßige Upgrades, die Sie gleich lernen werden — docker compose pull && docker compose up -d — und :latest bedeutet, dass Major-Version-Sprünge genau dann erfolgen, wenn sie upstream veröffentlicht werden, nicht wenn Sie es wählen. Bei PostgreSQL ist das kein theoretisches Szenario: Ein unerwarteter Sprung von 16 auf 17 führt dazu, dass der Container in einer Crash-Loop landet, weil das Datenverzeichnis inkompatibel ist; Postgres-Major-Upgrades erfordern ein Dump und Restore, keinen Neustart.

Fixieren Sie mindestens die Major-Version (postgres:16-alpine folgt auf 16.x Patch-Releases), und fixieren Sie Anwendungen auf ein exaktes Release wie miniflux/miniflux:2.2.9 — prüfen Sie die Releases-Seite des Projekts und verwenden Sie die aktuelle Version, wenn Sie die Datei schreiben. Ein Upgrade wird dann zu einer gezielten Änderung in einer Zeile, die in git diff sichtbar ist.

An 127.0.0.1 binden, da Docker ufw umgeht

"127.0.0.1:8080:8080" — Host-Adresse, Host-Port, Container-Port. Die meisten Tutorials schreiben "8080:8080", was eine Abkürzung für 0.0.0.0:8080:8080 ist: das Lauschen auf jeder Schnittstelle, einschließlich der öffentlichen.

Hier liegt die Falle, die fast jeden einmal erwischt. Docker veröffentlicht einen Port, indem es eine DNAT-Regel schreibt, die das Ziel des Pakets auf die interne IP des Containers umschreibt, bevor gefiltert wird. Das Paket nimmt den FORWARD-Pfad und berührt niemals INPUT, wo Ihre ufw-Regeln liegen. sudo ufw deny 8080 meldet Erfolg, ufw status zeigt an, dass der Port verweigert wurde, aber der Dienst antwortet weiterhin auf das gesamte Internet. Ihre Firewall ist nicht defekt; sie wird durch das Design umgangen. Warum Docker ufw umgeht und wie man Container-Traffic wirklich filtert erklärt den Mechanismus und die DOCKER-USER-Lösung für Ports, die öffentlich bleiben müssen.

Die Gewohnheit, die das Problem löst: Binden Sie veröffentlichte Ports an 127.0.0.1, sofern Sie keinen spezifischen Grund dagegen haben, und setzen Sie einen Reverse Proxy davor für alles, was für das Internet erreichbar sein soll. Genau das baut der Traefik Reverse Proxy Guide als nächsten Schritt nach dieser Seite auf — ein Container, der die Ports 80 und 443 besitzt und alles andere per Hostname mit TLS routet. (Kommen Sie von einem älteren Traefik v2 Setup? Der Traefik v2 zu v3 Migrationsguide deckt die Umbenennungen und Regeländerungen ab.)

Überprüfen Sie die Bindung nach dem Start des Stacks: sudo ss -tlnp | grep 8080 sollte 127.0.0.1:8080 zeigen, nicht 0.0.0.0:8080 oder *:8080.

Named Volumes vs. Bind Mounts

db-data:/var/lib/postgresql/data ist ein Named Volume: Docker erstellt und verwaltet ein Verzeichnis unter /var/lib/docker/volumes/ und mountet es in den Container. Die Alternative ist ein Bind Mount, ./data:/var/lib/postgresql/data, der einen von Ihnen gewählten Pfad auf dem Host abbildet.

Die Trennung, die sich in der Praxis bewährt: Named Volumes nur für Container, die Daten schreiben — vor allem Datenbanken, da Docker das Volume mit der vom Image erwarteten Ownership initialisiert und die Dateiberechtigungen funktionieren. Bind Mounts für Dateien, die Sie vom Host aus bearbeiten — Konfigurationsdateien, die Sie mit einem Texteditor bearbeiten, eine Medienbibliothek, die Sie per rsync hineinkopieren, alles, dessen Pfad offensichtlich sein soll. Das klassische Problem bei Bind Mounts ist die Ownership: Der Container läuft als UID 999, Ihr Host-Verzeichnis gehört UID 1000, und die App bricht beim Start mit permission denied in den Logs ab. Named Volumes lassen diese Art von Bug fast vollständig verschwinden, allerdings auf Kosten der Tatsache, dass die Daten unter einem von Docker verwalteten Pfad liegen — siehe unten.

environment und .env — Geheimnisse aus git fernhalten

${POSTGRES_PASSWORD} wird nicht aus Ihrer Shell gelesen; Compose interpoliert es aus einer Datei namens .env, die neben compose.yml liegt. Erstellen Sie diese:

cat > .env <<'EOF'
POSTGRES_PASSWORD=change-me-to-something-long
ADMIN_PASSWORD=change-me-too
EOF
chmod 600 .env
echo ".env" >> .gitignore

Generieren Sie echte Werte mit openssl rand -hex 24. Hexadezimal, nicht Base64, absichtlich: Dieses Passwort landet in der DATABASE_URL Connection-String, und die Zeichen /, + und =, die Base64 erzeugt, unterbrechen das URL-Parsing — ein Fehler, der als Authentifizierungsfehler und nicht als Syntaxfehler auftritt und einen Abend kostet. Die .gitignore-Zeile gehört vor den ersten Commit: Die Compose-Datei ist sicher zum Veröffentlichen und Versionieren, die .env-Datei hingegen niemals, und ein Geheimnis, das die Git-Historie berührt hat, muss rotiert werden. Wenn Sie den Stack mit einer fehlenden Variable starten, warnt Compose deutlich und fährt mit einem leeren String fort — was bei einem Postgres-Passwort ein fehlerhaftes Deployment bedeutet:

WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string.

docker compose config gibt die vollständig interpolierte Datei aus — der schnellste Weg zu prüfen, was die Container tatsächlich erhalten; denken Sie daran, dass die Ausgabe Ihre Geheimnisse enthält.

depends_on wartet auf nichts — es sei denn, Sie fügen einen Healthcheck hinzu

Ein nacktes depends_on: [db] steuert nur die Startreihenfolge: Compose startet Postgres zuerst und die App einen Moment später, während Postgres noch Sekunden davon entfernt ist, Verbindungen zu akzeptieren. Die App erreicht die Datenbank, schlägt fehl und stürzt ab oder versucht es erneut, je nachdem, wie gut sie geschrieben wurde.

Die zuverlässige Version nutzt das oben gezeigte Format: Der Service db definiert einen healthcheck (Postgres liefert pg_isready genau dafür mit), und die App deklariert depends_on mit condition: service_healthy. Compose startet die Datenbank, prüft den Status alle 10 Sekunden und startet Miniflux erst, wenn der Check erfolgreich ist. Wenn die Datenbank nie "healthy" wird — falsches Passwort, korruptes Volume — startet die App nie und Compose teilt Ihnen mit, welche Abhängigkeit fehlgeschlagen ist:

dependency failed to start: container miniflux-db-1 is unhealthy

Diese Meldung weist Sie auf docker compose logs db hin, wo der eigentliche Fehler liegt.

restart: unless-stopped

restart: unless-stopped für beide Services bedeutet, dass die Container nach einem Absturz und nach einem VPS-Reboot wieder starten, aber offline bleiben, wenn Sie explizit docker compose stop ausgeführt haben. Die Alternative always lässt Container sogar nach einem manuellen Stopp wieder aufleben — was selten beabsichtigt ist. Ohne eine Restart-Policy führt ein Kernel-Update-Reboot um 4 Uhr morgens dazu, dass Ihre Dienste lautlos offline gehen, bis Sie es bemerken.

Die täglichen Befehle

Alle täglichen Aufgaben werden mit fünf Befehlen im Projektverzeichnis ausgeführt.

docker compose up -d        # create and start; idempotent, recreates only what changed
docker compose ps           # status, ports, and health of this project's containers
docker compose logs -f miniflux   # follow one service's logs; --tail 100 for recent history
docker compose pull && docker compose up -d   # upgrade to the pinned tags
docker compose down         # stop and remove containers and the network

up -d kann wiederholt ausgeführt werden — es vergleicht die Datei mit dem Ist-Zustand und modifiziert nur Dienste, deren Konfiguration oder Image sich geändert hat. Das Upgrade-Paar lädt herunter, worauf Ihre Pin-Tags aktuell verweisen: Patch-Releases unter postgres:16-alpine; bei einem exakten Pin erfolgt keine Änderung, bis Sie diesen bearbeiten — das ist der Zweck. Nach Upgrades sammeln sich alte Images an; geben Sie mit docker image prune -f Speicherplatz frei.

Nun der destruktive Befehl: docker compose down ist sicher — Container und das Netzwerk sind entbehrlich, da Ihre Daten im Volume liegen. docker compose down -v löscht auch die benannten Volumes. Damit wird Ihre Datenbank sofort und ohne Bestätigung oder Rückgängigmachen gelöscht. Die Flag -v dient zum Abbruch von Experimenten; behandeln Sie sie bei einem Stack mit echten Daten so wie rm -rf. Unter /var/lib/docker/volumes/ gibt es keinen Papierkorb.

Für eine einmalige Shell in einem laufenden Container: docker compose exec db psql -U miniflux öffnet die Datenbank-Shell, und docker compose exec miniflux sh öffnet eine Shell in der App.

Wo Ihre Daten tatsächlich gespeichert sind

Benannte Volumes erhalten das Projekt-Präfix. Daher wird aus db-data in einem Verzeichnis namens miniflux miniflux_db-data:

docker volume ls
docker volume inspect miniflux_db-data

Die Ausgabe von inspect enthält die relevante Zeile:

"Mountpoint": "/var/lib/docker/volumes/miniflux_db-data/_data"

Dieses Verzeichnis ist die Datenbank — es gehört dem Benutzer root, liegt auf dem Host-Dateisystem und bleibt nach down, Upgrades und dem Neuaufbau von Containern erhalten. Dies ist exakt der Bereich, der durch Ihre Backups gesichert werden muss.

Sichern Sie ein benanntes Volume

Das Standardverfahren ist die Verwendung eines temporären Containers. Dieser mountet das Volume schreibgeschützt neben ein Host-Verzeichnis und führt ein tar-Archiv aus:

docker run --rm \
  -v miniflux_db-data:/data:ro \
  -v "$PWD":/backup \
  alpine:3.22 tar czf /backup/miniflux-db-$(date +%F).tar.gz -C /data .

Es ist keine Installation erforderlich, es laufen keine Hintergrundprozesse und die Wiederherstellung ist das Spiegelbild — tar xzf in ein neues, leeres Volume mit umgekehrten Mounts.

Ein Vorbehalt gilt für Datenbanken: Das Sichern eines laufenden Postgres-Datenverzeichnisses mit tar kann einen Zustand während eines Schreibvorgangs erfassen, der beim Neustart zu Fehlern führt. Entweder nutzen Sie docker compose stop für die Dauer des tar-Befehls oder — besser — erstellen Sie ein logisches Dump, da dieses konstruktionsbedingt konsistent ist:

docker compose exec -T db pg_dump -U miniflux miniflux | gzip > miniflux-$(date +%F).sql.gz

Der Parameter -T deaktiviert das Pseudo-Terminal, das Compose standardmäßig zuweist — das Umleiten von Dump-Ausgaben durch ein TTY kann diese beschädigen. Hinterlegen Sie einen dieser Befehle in cron und kopieren Sie das Ergebnis vom VPS weg; ein Backup auf derselben Festplatte wie die geschützten Daten ist lediglich eine Kopie, kein Backup. Der Nextcloud Guide erstellt eine vollständige Zeitplanung basierend auf genau diesen zwei Mustern.

Fehlerursachen und die entsprechenden Meldungen

permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock — Sie sind noch nicht in der Gruppe docker oder die Gruppenänderung wurde in der aktuellen Sitzung noch nicht übernommen. id zeigt die effektiven Gruppen an; newgrp docker behebt dies für die aktuelle Shell. Ein Logout und erneuter Login behebt alle Gruppenänderungen.

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? — ein anderes Problem: Der Daemon ist gestoppt. sudo systemctl status docker und sudo journalctl -u docker -n 50 geben den Grund an. Bei einem VPS ist die häufigste Ursache ein voller Datenträger — prüfen Sie zuerst df -h /var/lib/docker.

Bind for 127.0.0.1:8080 failed: port is already allocated — ein anderer Container belegt bereits diesen Host-Port. docker ps zeigt den betroffenen Container an; oft ist ein veralteter Container aus einem Experiment von docker run die Ursache. Wenn docker ps keine Ergebnisse liefert, belegt ein Nicht-Docker-Prozess den Port: sudo ss -tlnp | grep 8080 nennt den Prozess.

yaml: line 14: did not find expected key — ein Einrückungsfehler in oder direkt über der genannten Zeile. Compose-Dateien sind YAML-Dateien: Verwenden Sie zwei Leerzeichen zur Einrückung. Nur Leerzeichen sind erlaubt; ein Tabulator führt zu einem Fehler. docker compose config validiert die Datei ohne Startvorgang. Es ist ratsam, dies nach jeder Änderung durchzuführen.

Die ufw-Überraschung gibt keine Fehlermeldung aus, was die Konfiguration riskant macht: Das Deployment funktioniert, ufw status sieht korrekt aus, aber ein externer Portscan findet die Datenbank trotzdem. Lesen Sie den Abschnitt zu den Ports oben erneut, prüfen Sie jeden ports:-Eintrag auf ein fehlendes 127.0.0.1:-Präfix und testen Sie die Verbindung von einer anderen Maschine mit curl http://your-vps-ip:8080 — die Antwort muss "connection refused" lauten.

Ab hier verwandelt der Traefik-Leitfaden diesen einzelnen Stack in viele Anwendungen hinter einem einzigen HTTPS-Einstiegspunkt. Was es sich 2026 lohnt, selbst zu hosten ist die Liste der Anwendungen für diesen Aufbau.

Ein Game-Server wie ein Minecraft-Server auf einem VPS eignet sich gut als erstes Compose-Projekt zum Üben.

FAQ

Why do I get "permission denied while trying to connect to the Docker daemon socket"?

Your user is not in the docker group, or was added after the current session began — membership only applies at login. Run sudo usermod -aG docker $USER, then newgrp docker or log out and back in, and confirm with id. The group grants root-equivalent access to the host, so only add users you would give sudo.

Does docker compose down delete my data?

Plain docker compose down does not — it removes containers and the project network; named volumes survive and the next up -d reattaches them. docker compose down -v is the destructive form: it deletes the named volumes, meaning your database, with no confirmation and no undo. Never run -v on a stack with real data unless you hold a verified backup.

What is the difference between docker-compose and docker compose?

docker-compose (hyphen) is Compose v1, a standalone Python binary that reached end of life in 2023 and should not be installed on new servers. docker compose (space) is Compose v2, a Go plugin for the Docker CLI, installed as docker-compose-plugin from Docker's apt repository. Commands and YAML are almost fully compatible, so when an old tutorial says docker-compose up, type docker compose up.

Why can I reach my Docker container from the internet even though ufw blocks the port?

Because Docker publishes ports with DNAT rules in iptables' PREROUTING chain, and the rewritten packets travel the FORWARD path through Docker's own chains — they never hit the INPUT chain where ufw's rules apply. ufw deny 8080 therefore does nothing to a published container port. Fix it at the source: publish to 127.0.0.1: and expose services through a reverse proxy instead.

Should I use a named volume or a bind mount?

Named volumes for data only the container touches — databases especially, since Docker sets the ownership the image expects and permissions just work. Bind mounts for files you also handle from the host: configs you edit, media you upload, anything whose path you want obvious. If a container fails at startup with permission denied on a bind mount, host-vs-container UID mismatch is the first thing to check.