<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: serverküche</title>
    <description>The latest articles on DEV Community by serverküche (@serverkueche).</description>
    <link>https://dev.to/serverkueche</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4026385%2Fbcade448-c7e2-49fc-8e0f-176232f13294.jpg</url>
      <title>DEV Community: serverküche</title>
      <link>https://dev.to/serverkueche</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/serverkueche"/>
    <language>en</language>
    <item>
      <title>Docker Compose verstehen: Services, Volumes, Netzwerke</title>
      <dc:creator>serverküche</dc:creator>
      <pubDate>Fri, 17 Jul 2026 22:10:57 +0000</pubDate>
      <link>https://dev.to/serverkueche/docker-compose-verstehen-services-volumes-netzwerke-11je</link>
      <guid>https://dev.to/serverkueche/docker-compose-verstehen-services-volumes-netzwerke-11je</guid>
      <description>&lt;p&gt;Im &lt;a href="https://serverkueche.de/tutorials/docker-installieren/" rel="noopener noreferrer"&gt;Docker-Tutorial&lt;/a&gt; hast du das Compose-Plugin&lt;br&gt;
installiert – erklärt haben wir es noch nicht. Das holen wir jetzt nach, denn fast&lt;br&gt;
jedes App-Rezept hier beschreibt eine Anwendung als &lt;strong&gt;&lt;code&gt;compose.yaml&lt;/code&gt;&lt;/strong&gt;. Wer diese&lt;br&gt;
Datei liest wie einen Einkaufszettel, kann jedes folgende Tutorial anpassen, statt&lt;br&gt;
nur zu kopieren.&lt;/p&gt;
&lt;h2&gt;
  
  
  Was bauen wir?
&lt;/h2&gt;

&lt;p&gt;Wir bauen Schritt für Schritt einen kleinen Stack auf und lernen dabei die fünf&lt;br&gt;
Bausteine kennen, aus denen praktisch jede &lt;code&gt;compose.yaml&lt;/code&gt; besteht: &lt;strong&gt;Services&lt;/strong&gt;&lt;br&gt;
(die Container), &lt;strong&gt;Ports&lt;/strong&gt; (Erreichbarkeit von außen), &lt;strong&gt;Volumes&lt;/strong&gt; (persistente&lt;br&gt;
Daten), &lt;strong&gt;Netzwerke&lt;/strong&gt; (Container reden miteinander) und &lt;strong&gt;Umgebungsvariablen&lt;/strong&gt;&lt;br&gt;
(Konfiguration). Am Ende verstehst du, warum deine Daten einen &lt;code&gt;down&lt;/code&gt;-Befehl&lt;br&gt;
überleben – und wann nicht.&lt;/p&gt;

&lt;p&gt;Getestet mit &lt;strong&gt;Docker Compose v5.2&lt;/strong&gt; (das &lt;code&gt;docker compose&lt;/code&gt;-Plugin ohne Bindestrich –&lt;br&gt;
nicht das alte &lt;code&gt;docker-compose&lt;/code&gt; v1 mit Bindestrich).&lt;/p&gt;
&lt;h2&gt;
  
  
  Voraussetzungen
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ein &lt;a href="https://serverkueche.de/tutorials/ssh-absichern/" rel="noopener noreferrer"&gt;abgesicherter Server&lt;/a&gt; mit &lt;a href="https://serverkueche.de/tutorials/docker-installieren/" rel="noopener noreferrer"&gt;installiertem
Docker&lt;/a&gt; und Compose-Plugin&lt;/li&gt;
&lt;li&gt;Der Benutzer ist in der &lt;code&gt;docker&lt;/code&gt;-Gruppe (dann brauchst du kein &lt;code&gt;sudo&lt;/code&gt; vor
&lt;code&gt;docker&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Schritt für Schritt
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Schritt 1: Die erste compose.yaml
&lt;/h3&gt;

&lt;p&gt;Eine &lt;code&gt;compose.yaml&lt;/code&gt; beschreibt &lt;strong&gt;deklarativ&lt;/strong&gt;, welche Container laufen sollen – du&lt;br&gt;
sagst &lt;em&gt;was&lt;/em&gt; du willst, nicht &lt;em&gt;wie&lt;/em&gt;. Lege einen Projektordner an; der Ordnername&lt;br&gt;
wird später zum Präfix aller Container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/compose-demo/site &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ~/compose-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lege eine kleine HTML-Seite an, die wir gleich ausliefern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;h1&amp;gt;Hallo aus der Serverküche&amp;lt;/h1&amp;gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; site/index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Und jetzt die zentrale Datei &lt;code&gt;compose.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:1.31&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./site:/usr/share/nginx/html:ro&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zeile für Zeile:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;services:&lt;/code&gt;&lt;/strong&gt; – die oberste Ebene. Jeder Eintrag darunter ist ein Container.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;web:&lt;/code&gt;&lt;/strong&gt; – ein frei wählbarer &lt;strong&gt;Service-Name&lt;/strong&gt;. Merke ihn dir, er wird später
auch zum Hostnamen im internen Netzwerk (Schritt 3).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;image: nginx:1.31&lt;/code&gt;&lt;/strong&gt; – das Container-Image mit &lt;strong&gt;festem Tag&lt;/strong&gt;. Nie &lt;code&gt;latest&lt;/code&gt;
verwenden: &lt;code&gt;latest&lt;/code&gt; ändert sich unter dir und macht Fehler unreproduzierbar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ports: - "8080:80"&lt;/code&gt;&lt;/strong&gt; – Format &lt;code&gt;HOST:CONTAINER&lt;/code&gt;. Port &lt;strong&gt;80 im Container&lt;/strong&gt; wird
auf &lt;strong&gt;8080 des Servers&lt;/strong&gt; gelegt. Links steht immer der Server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;volumes: - ./site:...:ro&lt;/code&gt;&lt;/strong&gt; – der lokale Ordner &lt;code&gt;site&lt;/code&gt; wird ins Web-Root
gehängt, &lt;code&gt;:ro&lt;/code&gt; = read-only. Mehr dazu in Schritt 2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;restart: unless-stopped&lt;/code&gt;&lt;/strong&gt; – der Container startet nach einem Reboot oder
Absturz automatisch neu, außer du hast ihn selbst gestoppt. Für Server-Dienste
der sinnvolle Standard. Die Alternativen: &lt;code&gt;no&lt;/code&gt; (nie automatisch – der Default),
&lt;code&gt;always&lt;/code&gt; (startet selbst nach einem manuellen Stopp wieder, selten gewollt) und
&lt;code&gt;on-failure&lt;/code&gt; (nur nach einem Absturz mit Fehlercode). Für die allermeisten Dienste
ist &lt;code&gt;unless-stopped&lt;/code&gt; genau richtig.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Starte den Stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-d&lt;/code&gt; bedeutet &lt;strong&gt;detached&lt;/strong&gt; (im Hintergrund). Beim ersten Mal lädt Docker das Image;&lt;br&gt;
danach siehst du am Ende:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Network compose-demo_default  Created
 Container compose-demo-web-1  Started
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prüfe den Status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME                 IMAGE        COMMAND                  SERVICE   CREATED          STATUS          PORTS
compose-demo-web-1   nginx:1.31   "/docker-entrypoint.…"   web       10 seconds ago   Up 9 seconds    0.0.0.0:8080-&amp;gt;80/tcp, [::]:8080-&amp;gt;80/tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Der Container heißt &lt;code&gt;compose-demo-web-1&lt;/code&gt; – &lt;strong&gt;Projektordner + Service + Nummer&lt;/strong&gt;. Test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl localhost:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Hallo aus der Serverküche&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Läuft. Logs eines Dienstes siehst du mit &lt;code&gt;docker compose logs web&lt;/code&gt; (oder &lt;code&gt;-f&lt;/code&gt; zum&lt;br&gt;
Mitlaufen).&lt;/p&gt;
&lt;h3&gt;
  
  
  Schritt 2: Volumes – wo deine Daten wirklich liegen
&lt;/h3&gt;

&lt;p&gt;Container sind &lt;strong&gt;vergänglich&lt;/strong&gt;: Löschst du einen Container, ist alles weg, was&lt;br&gt;
&lt;em&gt;im&lt;/em&gt; Container geschrieben wurde. Damit Daten das überleben, gibt es zwei Arten von&lt;br&gt;
Volumes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bind-Mount&lt;/strong&gt; (&lt;code&gt;./site:/usr/share/nginx/html&lt;/code&gt;): ein &lt;strong&gt;Ordner von deinem Server&lt;/strong&gt;
wird in den Container gehängt. Ideal für Config-Dateien, die du selbst bearbeitest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Named Volume&lt;/strong&gt; (&lt;code&gt;webdata:/var/lib/...&lt;/code&gt;): ein von &lt;strong&gt;Docker verwalteter&lt;/strong&gt;
Speicher. Ideal für Datenbank-Daten – performant und sauber getrennt vom Host.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Schritt 1 haben wir ein Bind-Mount genutzt. Für datenbankartige Dienste sieht&lt;br&gt;
es so aus – ändere &lt;code&gt;compose.yaml&lt;/code&gt; testweise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:1.31&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;webdata:/usr/share/nginx/html&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;webdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Named Volumes müssen &lt;strong&gt;zusätzlich&lt;/strong&gt; auf oberster Ebene unter &lt;code&gt;volumes:&lt;/code&gt; deklariert&lt;br&gt;
werden. Nach &lt;code&gt;docker compose up -d&lt;/code&gt; taucht das Volume auf:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker volume &lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DRIVER    VOLUME NAME
local     compose-demo_webdata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Der entscheidende Punkt kommt jetzt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down
docker volume &lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Container compose-demo-web-1  Removed
 Network compose-demo_default  Removed
DRIVER    VOLUME NAME
local     compose-demo_webdata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;docker compose down&lt;/code&gt; entfernt Container und Netzwerk – &lt;strong&gt;das Volume bleibt&lt;/strong&gt;. Genau&lt;br&gt;
deshalb überleben deine Datenbank-Inhalte ein Update. Merke dir das Gegenstück:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🛑 down -v löscht Daten&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docker compose down -v&lt;/code&gt; löscht &lt;strong&gt;auch die Named Volumes&lt;/strong&gt; – also alle persistenten&lt;br&gt;
Daten des Stacks. Tippe das &lt;code&gt;-v&lt;/code&gt; nie aus Reflex mit. Für ein reines Neustarten&lt;br&gt;
reicht &lt;code&gt;docker compose down&lt;/code&gt; (ohne &lt;code&gt;-v&lt;/code&gt;).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Schritt 3: Netzwerke – Container reden miteinander
&lt;/h3&gt;

&lt;p&gt;Compose legt automatisch &lt;strong&gt;ein Netzwerk pro Projekt&lt;/strong&gt; an (oben gesehen:&lt;br&gt;
&lt;code&gt;compose-demo_default&lt;/code&gt;). Alle Services darin erreichen sich &lt;strong&gt;über ihren&lt;br&gt;
Service-Namen&lt;/strong&gt; als Hostname – kein IP-Gefummel nötig. Das ist der Grund, warum in&lt;br&gt;
App-Tutorials die Anwendung ihre Datenbank einfach unter &lt;code&gt;db&lt;/code&gt; erreicht.&lt;/p&gt;

&lt;p&gt;Zum Beweis ein zweiter Service, der den ersten anspricht:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:1.31&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./site:/usr/share/nginx/html:ro&lt;/span&gt;

  &lt;span class="na"&gt;ping&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;curlimages/curl:8.21.0&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;web&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-s"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://web"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;depends_on: - web&lt;/code&gt;&lt;/strong&gt; – Compose startet &lt;code&gt;web&lt;/code&gt; &lt;strong&gt;vor&lt;/strong&gt; &lt;code&gt;ping&lt;/code&gt;. (Achtung: das
wartet nur auf den &lt;em&gt;Start&lt;/em&gt;, nicht auf „fertig hochgefahren" – dafür gibt es
Healthchecks, Schritt 4.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;command:&lt;/code&gt;&lt;/strong&gt; – überschreibt den Standardbefehl des Images. &lt;code&gt;ping&lt;/code&gt; ruft
&lt;code&gt;http://web&lt;/code&gt; auf – &lt;strong&gt;&lt;code&gt;web&lt;/code&gt; ist der Service-Name aus derselben Datei&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Führe nur den &lt;code&gt;ping&lt;/code&gt;-Service einmalig aus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; ping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Hallo aus der Serverküche&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ping&lt;/code&gt; hat &lt;code&gt;web&lt;/code&gt; allein über den Namen erreicht – ganz ohne Port-Mapping. &lt;strong&gt;Merke:&lt;br&gt;
&lt;code&gt;ports:&lt;/code&gt; brauchst du nur, um einen Dienst von &lt;em&gt;außen&lt;/em&gt; (dem Internet) erreichbar zu&lt;br&gt;
machen.&lt;/strong&gt; Container untereinander reden über das interne Netzwerk – später lassen&lt;br&gt;
wir deshalb Datenbanken bewusst &lt;em&gt;ohne&lt;/em&gt; &lt;code&gt;ports:&lt;/code&gt; laufen.&lt;/p&gt;

&lt;p&gt;Bisher lebt jedes Netzwerk &lt;strong&gt;innerhalb&lt;/strong&gt; eines Compose-Projekts. Manchmal sollen aber&lt;br&gt;
Container aus &lt;strong&gt;verschiedenen&lt;/strong&gt; Projekten miteinander reden – das klassische Beispiel ist&lt;br&gt;
ein &lt;strong&gt;Reverse Proxy&lt;/strong&gt;, der vor vielen unabhängigen App-Stacks sitzt. Dafür gibt es das&lt;br&gt;
&lt;strong&gt;externe Netzwerk&lt;/strong&gt;: eines, das du &lt;strong&gt;einmalig von Hand&lt;/strong&gt; anlegst und das danach mehrere&lt;br&gt;
Compose-Projekte gemeinsam nutzen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker network create proxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In der &lt;code&gt;compose.yaml&lt;/code&gt; legst du es dann nicht neu an, sondern verweist mit &lt;code&gt;external: true&lt;/code&gt;&lt;br&gt;
auf das bereits bestehende Netzwerk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:1.31&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;proxy&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;external: true&lt;/code&gt; sagt Compose: „Dieses Netzwerk existiert schon – leg es &lt;strong&gt;nicht&lt;/strong&gt; an und&lt;br&gt;
lösch es beim &lt;code&gt;down&lt;/code&gt; &lt;strong&gt;nicht&lt;/strong&gt;." Fehlt das Netzwerk, bricht &lt;code&gt;up&lt;/code&gt; mit &lt;code&gt;network proxy&lt;br&gt;
declared as external, but could not be found&lt;/code&gt; ab – dann hast du das &lt;code&gt;docker network create&lt;/code&gt;&lt;br&gt;
vergessen. Genau dieses Muster – ein gemeinsames &lt;code&gt;proxy&lt;/code&gt;-Netz plus &lt;code&gt;external: true&lt;/code&gt; – ist&lt;br&gt;
die Grundlage des &lt;a href="https://serverkueche.de/tutorials/reverse-proxy-traefik/" rel="noopener noreferrer"&gt;Traefik-Tutorials&lt;/a&gt;, mit dem jede App&lt;br&gt;
später ihre Domain und ihr HTTPS bekommt.&lt;/p&gt;
&lt;h3&gt;
  
  
  Schritt 4: Konfiguration – Umgebungsvariablen, .env und Healthchecks
&lt;/h3&gt;

&lt;p&gt;Fast jede Anwendung wird über &lt;strong&gt;Umgebungsvariablen&lt;/strong&gt; konfiguriert. Zwei Wege:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;beispiel/app:1.0&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TZ=Europe/Berlin&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;APP_PORT=3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Geheimnisse (Passwörter, Tokens) gehören &lt;strong&gt;nicht&lt;/strong&gt; in die &lt;code&gt;compose.yaml&lt;/code&gt;, sondern in&lt;br&gt;
eine &lt;code&gt;.env&lt;/code&gt;-Datei im selben Ordner. Compose liest sie automatisch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"DB_PASSWORD=EIN_LANGES_ZUFALLSPASSWORT"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:18&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD=${DB_PASSWORD}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ &lt;code&gt;.env&lt;/code&gt; nie ins Backup-Repo pushen&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Die &lt;code&gt;.env&lt;/code&gt; enthält Klartext-Geheimnisse. Nimm sie in eine &lt;code&gt;.gitignore&lt;/code&gt; auf, falls du&lt;br&gt;
deine Compose-Dateien versionierst, und sichere sie getrennt (verschlüsselt).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So prüfst du, ob Compose deine Datei versteht &lt;strong&gt;und&lt;/strong&gt; die &lt;code&gt;.env&lt;/code&gt;-Werte richtig&lt;br&gt;
einsetzt – ohne etwas zu starten:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;config&lt;/code&gt; löst alle Variablen auf und gibt die fertige, normalisierte Konfiguration&lt;br&gt;
aus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;compose-demo&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EIN_LANGES_ZUFALLSPASSWORT&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:18&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Steht dort dein &lt;strong&gt;echter&lt;/strong&gt; Wert statt &lt;code&gt;${DB_PASSWORD}&lt;/code&gt;, greift die &lt;code&gt;.env&lt;/code&gt;. Brauchst&lt;br&gt;
du nur einen schnellen Syntax-Check ohne die ganze Ausgabe, nimm&lt;br&gt;
&lt;code&gt;docker compose config --quiet&lt;/code&gt; – kommt nichts zurück (Exit-Code &lt;code&gt;0&lt;/code&gt;), ist die Datei&lt;br&gt;
gültig. Genau das ist auch dein erster Griff bei YAML-Fehlern (siehe unten).&lt;/p&gt;

&lt;p&gt;Ein &lt;strong&gt;Healthcheck&lt;/strong&gt; sagt Docker, wann ein Dienst wirklich bereit ist – die Basis&lt;br&gt;
dafür, dass abhängige Dienste erst dann starten:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:18&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD=${DB_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;postgres"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;beispiel/app:1.0&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mit &lt;code&gt;condition: service_healthy&lt;/code&gt; startet &lt;code&gt;app&lt;/code&gt; erst, wenn der Healthcheck von &lt;code&gt;db&lt;/code&gt;&lt;br&gt;
grün ist – das häufigste „warum verbindet sich meine App nicht zur Datenbank?"&lt;br&gt;
verschwindet damit. Die vier Healthcheck-Felder bedeuten: &lt;strong&gt;&lt;code&gt;test&lt;/code&gt;&lt;/strong&gt; ist der Befehl,&lt;br&gt;
der im Container läuft (Exit-Code &lt;code&gt;0&lt;/code&gt; = gesund), &lt;strong&gt;&lt;code&gt;interval&lt;/code&gt;&lt;/strong&gt; der Abstand zwischen&lt;br&gt;
den Prüfungen, &lt;strong&gt;&lt;code&gt;timeout&lt;/code&gt;&lt;/strong&gt; wie lange eine Prüfung dauern darf, und &lt;strong&gt;&lt;code&gt;retries&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
wie viele Fehlversuche in Folge nötig sind, bevor der Container als &lt;code&gt;unhealthy&lt;/code&gt; gilt.&lt;br&gt;
Den aktuellen Zustand zeigt die &lt;code&gt;STATUS&lt;/code&gt;-Spalte von &lt;code&gt;docker compose ps&lt;/code&gt; als&lt;br&gt;
&lt;code&gt;(healthy)&lt;/code&gt; bzw. &lt;code&gt;(unhealthy)&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Schritt 5: Der Betriebs-Werkzeugkasten
&lt;/h3&gt;

&lt;p&gt;Diese Befehle brauchst du täglich – immer &lt;strong&gt;im Projektordner&lt;/strong&gt; ausführen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;        &lt;span class="c"&gt;# starten / Änderungen anwenden&lt;/span&gt;
docker compose ps           &lt;span class="c"&gt;# Status der Services&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; web  &lt;span class="c"&gt;# Logs live mitlesen (Strg+C beendet nur das Ansehen)&lt;/span&gt;
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;web sh  &lt;span class="c"&gt;# Shell im laufenden Container&lt;/span&gt;
docker compose restart web  &lt;span class="c"&gt;# einen einzelnen Dienst neu starten&lt;/span&gt;
docker compose stop         &lt;span class="c"&gt;# anhalten, ohne Container/Netzwerk zu entfernen&lt;/span&gt;
docker compose pull         &lt;span class="c"&gt;# neue Image-Versionen holen&lt;/span&gt;
docker compose down         &lt;span class="c"&gt;# Stack stoppen und entfernen (Volumes bleiben)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fast alle Befehle lassen sich auf &lt;strong&gt;einen&lt;/strong&gt; Service einschränken, indem du seinen&lt;br&gt;
Namen anhängst (&lt;code&gt;docker compose logs -f web&lt;/code&gt;, &lt;code&gt;docker compose restart web&lt;/code&gt;) – ohne&lt;br&gt;
Namen gelten sie für den ganzen Stack. Der Unterschied zwischen &lt;code&gt;stop&lt;/code&gt; und &lt;code&gt;down&lt;/code&gt;:&lt;br&gt;
&lt;code&gt;stop&lt;/code&gt; hält die Container nur an (mit &lt;code&gt;start&lt;/code&gt; geht's weiter), &lt;code&gt;down&lt;/code&gt; entfernt sie&lt;br&gt;
samt Netzwerk (die Named Volumes bleiben in beiden Fällen).&lt;/p&gt;

&lt;p&gt;Ein Update läuft fast immer nach demselben Muster: Tag in der &lt;code&gt;compose.yaml&lt;/code&gt;&lt;br&gt;
hochsetzen → &lt;code&gt;docker compose pull&lt;/code&gt; → &lt;code&gt;docker compose up -d&lt;/code&gt;. Compose ersetzt nur die&lt;br&gt;
Container, deren Image sich geändert hat.&lt;/p&gt;
&lt;h3&gt;
  
  
  Schritt 6: Alles zusammen – ein realistischer App-Stack
&lt;/h3&gt;

&lt;p&gt;So sieht das Muster aus, das dir in den App-Tutorials immer wieder begegnet: eine&lt;br&gt;
Anwendung plus ihre Datenbank. Diese Datei bündelt alles aus den Schritten 1–4 –&lt;br&gt;
lies sie einmal komplett, dann hast du 90 % jeder späteren &lt;code&gt;compose.yaml&lt;/code&gt;&lt;br&gt;
verstanden:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;beispiel/app:1.4&lt;/span&gt;          &lt;span class="c1"&gt;# feste Version, kein latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:3000"&lt;/span&gt;                  &lt;span class="c1"&gt;# nur die App ist von außen erreichbar&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TZ=Europe/Berlin&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_URL=postgres://app:${DB_PASSWORD}@db:5432/app&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;appdata:/data&lt;/span&gt;                &lt;span class="c1"&gt;# persistente App-Daten (Named Volume)&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;   &lt;span class="c1"&gt;# startet erst, wenn db bereit ist&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:18&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_USER=app&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD=${DB_PASSWORD}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_DB=app&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dbdata:/var/lib/postgresql/data&lt;/span&gt;   &lt;span class="c1"&gt;# die eigentlichen Datenbank-Dateien&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;app"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="c1"&gt;# kein ports: – die Datenbank ist NUR intern über den Namen "db" erreichbar&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;appdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dbdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drei Design-Entscheidungen, die du dir merken solltest:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Nur &lt;code&gt;app&lt;/code&gt; hat &lt;code&gt;ports:&lt;/code&gt;.&lt;/strong&gt; Die Datenbank braucht keinen offenen Host-Port – die
App erreicht sie intern über den Hostnamen &lt;code&gt;db&lt;/code&gt; (die &lt;code&gt;DATABASE_URL&lt;/code&gt; zeigt genau
dorthin). Ein nicht veröffentlichter Port ist ein Port, den niemand aus dem
Internet angreifen kann.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zwei getrennte Named Volumes.&lt;/strong&gt; App-Daten und Datenbank-Dateien liegen sauber
getrennt – das macht spätere Backups und Restores nachvollziehbar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passwort nur als &lt;code&gt;${DB_PASSWORD}&lt;/code&gt;.&lt;/strong&gt; Der echte Wert steht in der &lt;code&gt;.env&lt;/code&gt;, nicht
in dieser Datei. Dieselbe &lt;code&gt;compose.yaml&lt;/code&gt; kann so gefahrlos geteilt werden.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Genau dieses Grundgerüst – App nach außen, Datenbank nur intern, Daten in Named&lt;br&gt;
Volumes, Secrets in der &lt;code&gt;.env&lt;/code&gt; – wiederholt sich in Nextcloud, Vaultwarden,&lt;br&gt;
Paperless und den meisten anderen Rezepten.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wenn es nicht funktioniert
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; &lt;code&gt;yaml: line 7: did not find expected key&lt;/code&gt; (oder ähnliche YAML-Fehler)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; YAML ist &lt;strong&gt;einrückungssensibel&lt;/strong&gt; – ausschließlich Leerzeichen,&lt;br&gt;
&lt;strong&gt;niemals Tabs&lt;/strong&gt;, und pro Ebene konsistent (üblich: 2 Leerzeichen). Prüfe die Datei&lt;br&gt;
ohne sie zu starten: &lt;code&gt;docker compose config&lt;/code&gt; löst alles auf und meckert genau die&lt;br&gt;
falsche Zeile an.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; &lt;code&gt;Error ... address already in use&lt;/code&gt; beim &lt;code&gt;up&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; Der Host-Port (links in &lt;code&gt;8080:80&lt;/code&gt;) ist schon belegt. Finde den&lt;br&gt;
Beleger mit &lt;code&gt;sudo ss -tlnp | grep 8080&lt;/code&gt; oder wähle einen anderen Host-Port. Zwei&lt;br&gt;
Container dürfen sich denselben Host-Port nicht teilen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Eine App findet ihre Datenbank nicht (&lt;code&gt;could not translate host name&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; Als Hostname muss der &lt;strong&gt;Service-Name&lt;/strong&gt; stehen (z. B. &lt;code&gt;db&lt;/code&gt;),&lt;br&gt;
nicht &lt;code&gt;localhost&lt;/code&gt;. Innerhalb eines Containers ist &lt;code&gt;localhost&lt;/code&gt; der Container selbst,&lt;br&gt;
nicht der Nachbar-Service. Und: Beide Services müssen im selben Compose-Projekt&lt;br&gt;
(derselben Datei) liegen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Nach &lt;code&gt;docker compose down&lt;/code&gt; sind alle Daten weg.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; Entweder lag das Volume nicht als &lt;strong&gt;Named Volume&lt;/strong&gt; unter&lt;br&gt;
&lt;code&gt;volumes:&lt;/code&gt; vor (dann war es nur der vergängliche Container-Speicher), oder es wurde&lt;br&gt;
&lt;code&gt;down -v&lt;/code&gt; verwendet. Persistente Dienste immer mit deklariertem Named Volume fahren.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; &lt;code&gt;docker-compose: command not found&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; Das ist das alte Compose v1 (mit Bindestrich). Aktuell ist&lt;br&gt;
&lt;code&gt;docker compose&lt;/code&gt; (mit Leerzeichen, Plugin). Falls es fehlt:&lt;br&gt;
&lt;code&gt;sudo apt install docker-compose-plugin&lt;/code&gt; (siehe &lt;a href="https://serverkueche.de/tutorials/docker-installieren/" rel="noopener noreferrer"&gt;Docker-Tutorial&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Wartung &amp;amp; Backups
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Image-Tags pflegen:&lt;/strong&gt; Feste Tags (&lt;code&gt;nginx:1.31&lt;/code&gt;) bedeuten, dass du Updates
&lt;strong&gt;bewusst&lt;/strong&gt; durch Hochsetzen des Tags einspielst. Das ist gewollt – so entscheidest
du, wann ein Update kommt, statt überrascht zu werden. Rechne mit &lt;strong&gt;monatlichem&lt;/strong&gt;
Draufschauen auf die Release-Notes deiner Dienste.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Was gehört ins Backup?&lt;/strong&gt; Nicht die Container – die sind aus der &lt;code&gt;compose.yaml&lt;/code&gt;
jederzeit neu baubar. Sichern musst du &lt;strong&gt;die Named Volumes&lt;/strong&gt; (bzw. Bind-Mount-
Ordner), die &lt;code&gt;compose.yaml&lt;/code&gt; und die &lt;code&gt;.env&lt;/code&gt;. Ein durchdachtes Off-Site-Backup dieser
Daten bauen wir im Tutorial zu verschlüsselten Restic-Backups (folgt in der Serie).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aufräumen:&lt;/strong&gt; &lt;code&gt;docker compose down&lt;/code&gt; beim Abbau eines Stacks; ungenutzte Images
räumst du mit &lt;code&gt;docker image prune&lt;/code&gt; weg. Named Volumes werden &lt;strong&gt;nie&lt;/strong&gt; automatisch
gelöscht – das ist Absicht.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Damit kennst du das Vokabular jeder &lt;code&gt;compose.yaml&lt;/code&gt;. Der nächste Schritt ist der&lt;br&gt;
wichtigste Baustein der Serverküche: ein &lt;strong&gt;Reverse Proxy mit Traefik&lt;/strong&gt;, der deine&lt;br&gt;
Dienste unter echten Domains mit automatischem HTTPS erreichbar macht.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Dieser Beitrag erschien zuerst auf &lt;a href="https://serverkueche.de/tutorials/docker-compose-grundlagen/" rel="noopener noreferrer"&gt;serverkueche.de&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>tutorial</category>
      <category>german</category>
    </item>
  </channel>
</rss>
