YAML: Die Auszeichnungssprache in der Docker-Compose-Welt: Unterschied zwischen den Versionen
Admin (Diskussion | Beiträge) Die Seite wurde neu angelegt: „== docker-compose.yml — Referenz und Konzepte == Dieser Artikel erklärt den Aufbau und die Direktiven einer <code>docker-compose.yml</code> auf Deutsch. Er richtet sich an alle die Docker Compose produktiv einsetzen und eine kompakte Nachschlageseite in ihrer eigenen Sprache brauchen. Die offizielle Referenz ist ausschließlich auf Englisch verfügbar: [https://docs.docker.com/compose/compose-file/ docs.docker.com/compose/compose-file] ---- == YAML…“ |
Admin (Diskussion | Beiträge) |
||
| (Eine dazwischenliegende Version desselben Benutzers wird nicht angezeigt) | |||
| Zeile 1: | Zeile 1: | ||
== | == YAML — Die Auszeichnungssprache in der Docker-Compose-Welt == | ||
YAML steht für „YAML Ain't Markup Language" — ein rekursives Akronym, typisch für | |||
die Open-Source-Welt. Es ist eine Sprache zur Darstellung strukturierter Daten, | |||
die für Menschen lesbar sein soll. | |||
---- | |||
== Geschichte == | |||
YAML wurde 2001 von drei Entwicklern entworfen: Clark Evans, Ingy döt Net | |||
und Oren Ben-Kiki. Das ursprüngliche Akronym lautete noch „Yet Another Markup | |||
Language" — in Anlehnung an die vielen XML-Varianten der Zeit. Es wurde bald | |||
zu „YAML Ain't Markup Language" umgedeutet, um klarzustellen: das hier ist | |||
keine Dokumentensprache wie HTML oder XML, sondern eine reine Datensprache. | |||
Das Grundproblem das YAML lösen wollte war real: XML war für Konfigurationsdateien | |||
viel zu geschwätzig — jeder Wert brauchte ein öffnendes und schließendes Tag. | |||
JSON existierte noch gar nicht (das kam erst 2002). Man wollte etwas das Menschen | |||
wirklich lesen und schreiben können, ohne ständig spitze Klammern tippen zu müssen. | |||
=== Verbreitung vor Docker === | |||
Docker hat YAML nicht erfunden — es hat YAML einer neuen Generation bekannt gemacht. | |||
Vorher war YAML in anderen Welten zuhause: | |||
* '''Ruby on Rails''' (2004) — der erste große Durchbruch. Rails verwendete YAML | |||
für Datenbankverbindungen, Übersetzungen und Konfiguration. Eine ganze Generation | |||
von Webentwicklern lernte YAML über Rails, lange vor Docker. | |||
* '''Ansible''' (2012) — das Konfigurationsmanagement-Tool wählte YAML als zentrale | |||
Sprache für Playbooks. Für Systemadministratoren war Ansible der eigentliche | |||
YAML-Einstieg. | |||
* '''Kubernetes''' (2014) — parallel zu Docker entstanden, ebenfalls YAML für alles. | |||
Kubernetes hat YAML in der Infrastrukturwelt zementiert. | |||
* '''CI/CD-Systeme''' — GitHub Actions, GitLab CI, Travis CI, CircleCI — alle | |||
verwenden YAML für Pipeline-Definitionen. | |||
* '''OpenAPI/Swagger''' — API-Dokumentation wird seit Jahren in YAML geschrieben. | |||
Docker Compose hat YAML dann für Heimserver-Betreiber und Administratoren ohne | |||
Rails- oder Ansible-Hintergrund zum Standard gemacht. | |||
=== Kritik in der Fachwelt === | |||
YAML hat einen schlechten Ruf in Teilen der Entwicklergemeinde — nicht wegen | |||
der Grundidee, sondern wegen der Implementierung. | |||
Die Spezifikation ist notorisch komplex: YAML 1.2 umfasst über 80 Seiten. | |||
Verschiedene Parser verhalten sich bei Grenzfällen unterschiedlich. Und es gibt | |||
berühmte Stolperfallen: | |||
<pre> | |||
# Diese Werte sind KEIN String — YAML interpretiert sie als Boolean: | |||
land: no # → false | |||
antwort: yes # → true | |||
aktiv: on # → true | |||
# Das hier ist kein String — es wird zur Zahl: | |||
version: 1.0 # → Float 1.0, nicht String "1.0" | |||
port: 8080 # → Integer | |||
</pre> | |||
Das bekannteste Beispiel ist das sogenannte '''Norway Problem''': das Länderkürzel | |||
<code>NO</code> wird in älteren YAML-Parsern automatisch zu <code>false</code> | |||
— was dazu geführt hat dass norwegische Entwickler in Konfigurationsdateien | |||
ihren eigenen Ländercode nicht als String verwenden konnten ohne Anführungszeichen.🤣 | |||
YAML 1.2 (2009) hat viele dieser Probleme behoben, aber ältere Parser sind | |||
noch weit verbreitet. | |||
Das Fazit vieler erfahrener Entwickler: YAML ist für Menschen gut lesbar, | |||
aber für Programme schwer korrekt zu implementieren. Für Konfigurationsdateien | |||
die selten geändert werden ist es hervorragend — für Programme die YAML | |||
generieren oder parsen müssen, ist es eine Quelle von Überraschungen. | |||
---- | |||
== YAML und JSON — Verwandtschaft, kein Dialekt == | |||
Der häufige Vergleich „YAML ist ein JSON-Dialekt" ist ungenau. | |||
„YAML ist ein JSON-Dialekt" ist ungenau: | Die präzise Formulierung: '''YAML ist eine Obermenge von JSON.''' | ||
* Jedes gültige JSON ist auch gültiges YAML | * Jedes gültige JSON ist auch gültiges YAML. | ||
* Umgekehrt gilt das nicht: YAML kann Dinge ausdrücken die JSON nicht kann | * Umgekehrt gilt das nicht: YAML kann Dinge ausdrücken die JSON nicht kann | ||
(Kommentare, mehrzeilige Strings, Anker und Referenzen). | (Kommentare, mehrzeilige Strings, Anker und Referenzen). | ||
| Zeile 29: | Zeile 92: | ||
Dasselbe Datenkonstrukt in beiden Sprachen: | Dasselbe Datenkonstrukt in beiden Sprachen: | ||
< | JSON: | ||
<pre> | |||
{ | { | ||
"services": { | "services": { | ||
| Zeile 38: | Zeile 102: | ||
} | } | ||
} | } | ||
</ | </pre> | ||
< | YAML: | ||
<pre> | |||
services: | services: | ||
nextcloud: | nextcloud: | ||
image: nextcloud:latest | image: nextcloud:latest | ||
restart: unless-stopped | restart: unless-stopped | ||
</ | </pre> | ||
YAML ist lesbarer — auf Kosten einer strengen | YAML ist lesbarer — auf Kosten einer strengen Regel: | ||
mit Leerzeichen, niemals mit Tabs.''' Ein falsch gesetzter Tab ist der häufigste | '''Einrückung mit Leerzeichen, niemals mit Tabs.''' | ||
Fehler in compose-Dateien. | Ein falsch gesetzter Tab ist der häufigste Fehler in compose-Dateien. | ||
=== Kommentare === | === Kommentare === | ||
| Zeile 57: | Zeile 122: | ||
pflegen: | pflegen: | ||
< | <pre> | ||
# Dieser Container darf keinen direkten Außenzugriff haben | # Dieser Container darf keinen direkten Außenzugriff haben | ||
ports: [] # keine ports → kein direkter Zugriff von außen | ports: [] # keine ports → kein direkter Zugriff von außen | ||
</ | </pre> | ||
=== Datentypen === | === Datentypen === | ||
| Zeile 82: | Zeile 147: | ||
|} | |} | ||
Stolperfalle: <code>restart: always</code> ist ein String. <code>restart: true</code> | Stolperfalle: <code>restart: always</code> ist ein String. | ||
wäre ein Boolean — und ungültig. Im Zweifel Anführungszeichen setzen. | <code>restart: true</code> wäre ein Boolean — und ungültig. | ||
Im Zweifel Anführungszeichen setzen. | |||
---- | ---- | ||
| Zeile 92: | Zeile 158: | ||
auf oberster Ebene: | auf oberster Ebene: | ||
< | <pre> | ||
services: # Pflicht — definiert die Container | services: # Pflicht — definiert die Container | ||
... | ... | ||
| Zeile 104: | Zeile 170: | ||
configs: # Optional — definiert Konfigurationsdateien | configs: # Optional — definiert Konfigurationsdateien | ||
... | ... | ||
</ | </pre> | ||
Der <code>services</code>-Abschnitt ist der einzig verpflichtende. | Der <code>services</code>-Abschnitt ist der einzig verpflichtende. | ||
| Zeile 114: | Zeile 180: | ||
Zwei Leerzeichen pro Ebene sind Konvention: | Zwei Leerzeichen pro Ebene sind Konvention: | ||
< | <pre> | ||
services: # Ebene 1 | services: # Ebene 1 | ||
nextcloud: # Ebene 2 — Name des Dienstes | nextcloud: # Ebene 2 — Name des Dienstes | ||
| Zeile 120: | Zeile 186: | ||
networks: # Ebene 3 — Direktive (Liste folgt) | networks: # Ebene 3 — Direktive (Liste folgt) | ||
- tuxi_net # Ebene 4 — Listenelement | - tuxi_net # Ebene 4 — Listenelement | ||
</ | </pre> | ||
---- | ---- | ||
| Zeile 130: | Zeile 196: | ||
Das Docker-Image das für den Container verwendet wird. | Das Docker-Image das für den Container verwendet wird. | ||
< | <pre> | ||
services: | services: | ||
nextcloud: | nextcloud: | ||
image: lscr.io/linuxserver/nextcloud:latest | image: lscr.io/linuxserver/nextcloud:latest | ||
</ | </pre> | ||
Format: <code>registry/image:tag</code>. Ohne Registry wird Docker Hub angenommen. | Format: <code>registry/image:tag</code>. Ohne Registry wird Docker Hub angenommen. | ||
| Zeile 145: | Zeile 211: | ||
einen Namen aus Projektname + Servicename + Nummer. | einen Namen aus Projektname + Servicename + Nummer. | ||
< | <pre> | ||
services: | services: | ||
nextcloud: | nextcloud: | ||
container_name: nextcloud | container_name: nextcloud | ||
</ | </pre> | ||
Wichtig: der Container-Name ist gleichzeitig der Docker-interne DNS-Name | Wichtig: der Container-Name ist gleichzeitig der Docker-interne DNS-Name | ||
| Zeile 158: | Zeile 224: | ||
Neustart-Verhalten des Containers. | Neustart-Verhalten des Containers. | ||
< | <pre> | ||
restart: unless-stopped | restart: unless-stopped | ||
</ | </pre> | ||
{| class="wikitable" | {| class="wikitable" | ||
| Zeile 178: | Zeile 244: | ||
Statt ein fertiges Image zu verwenden kann Docker Compose auch selbst eines bauen: | Statt ein fertiges Image zu verwenden kann Docker Compose auch selbst eines bauen: | ||
< | <pre> | ||
services: | services: | ||
mein-dienst: | mein-dienst: | ||
| Zeile 184: | Zeile 250: | ||
context: ./mein-verzeichnis # Pfad zum Dockerfile | context: ./mein-verzeichnis # Pfad zum Dockerfile | ||
dockerfile: Dockerfile.prod # optional: anderer Dateiname | dockerfile: Dockerfile.prod # optional: anderer Dateiname | ||
</ | </pre> | ||
=== ports === | === ports === | ||
| Zeile 190: | Zeile 256: | ||
Portweiterleitung zwischen Host und Container: <code>HOST:CONTAINER</code>. | Portweiterleitung zwischen Host und Container: <code>HOST:CONTAINER</code>. | ||
< | <pre> | ||
services: | services: | ||
caddy: | caddy: | ||
| Zeile 197: | Zeile 263: | ||
- "443:443" | - "443:443" | ||
- "443:443/udp" # für HTTP/3 | - "443:443/udp" # für HTTP/3 | ||
</ | </pre> | ||
'''Wichtig:''' Wer <code>ports</code> weglässt, ist nur im Docker-Netzwerk erreichbar — | '''Wichtig:''' Wer <code>ports</code> weglässt, ist nur im Docker-Netzwerk erreichbar — | ||
| Zeile 206: | Zeile 272: | ||
Einbinden von Verzeichnissen oder benannten Volumes in den Container. | Einbinden von Verzeichnissen oder benannten Volumes in den Container. | ||
< | <pre> | ||
services: | services: | ||
nextcloud: | nextcloud: | ||
volumes: | volumes: | ||
- ./nextcloud/app:/config | - ./nextcloud/app:/config # Bind Mount: Host-Pfad → Container-Pfad | ||
- /data/nextcloud-data:/data | - /data/nextcloud-data:/data # absoluter Host-Pfad | ||
- caddy_data:/data | - caddy_data:/data # benanntes Volume (unten definiert) | ||
volumes: | volumes: | ||
caddy_data: | caddy_data: # Docker verwaltet den Speicherort | ||
</ | </pre> | ||
{| class="wikitable" | {| class="wikitable" | ||
| Zeile 232: | Zeile 298: | ||
Umgebungsvariablen die in den Container übergeben werden. | Umgebungsvariablen die in den Container übergeben werden. | ||
< | <pre> | ||
services: | services: | ||
nextcloud: | nextcloud: | ||
| Zeile 240: | Zeile 306: | ||
- TZ=Europe/Berlin | - TZ=Europe/Berlin | ||
- MYSQL_PASSWORD=${MYSQL_PASSWORD} # Wert aus .env-Datei | - MYSQL_PASSWORD=${MYSQL_PASSWORD} # Wert aus .env-Datei | ||
</ | </pre> | ||
Alternativ als Map-Schreibweise: | Alternativ als Map-Schreibweise: | ||
< | <pre> | ||
environment: | environment: | ||
PUID: "1000" | PUID: "1000" | ||
TZ: Europe/Berlin | TZ: Europe/Berlin | ||
</ | </pre> | ||
=== env_file === | === env_file === | ||
Umgebungsvariablen aus einer externen Datei laden | Umgebungsvariablen aus einer externen Datei laden — sinnvoll für Passwörter: | ||
< | <pre> | ||
services: | services: | ||
nc-db: | nc-db: | ||
env_file: | env_file: | ||
- .env | - .env | ||
</ | </pre> | ||
Die <code>.env</code>-Datei enthält dann: | Die <code>.env</code>-Datei enthält dann: | ||
< | <pre> | ||
MYSQL_ROOT_PASSWORD=sehrgeheim | MYSQL_ROOT_PASSWORD=sehrgeheim | ||
MYSQL_DATABASE=nextcloud | MYSQL_DATABASE=nextcloud | ||
MYSQL_USER=ncuser | MYSQL_USER=ncuser | ||
MYSQL_PASSWORD= | MYSQL_PASSWORD=auchgeheim | ||
</ | </pre> | ||
Die <code>.env</code>-Datei gehört in <code>.gitignore</code> — niemals in ein | Die <code>.env</code>-Datei gehört in <code>.gitignore</code> — niemals in ein | ||
| Zeile 278: | Zeile 343: | ||
Netzwerkzugehörigkeit des Containers. | Netzwerkzugehörigkeit des Containers. | ||
< | <pre> | ||
services: | services: | ||
nextcloud: | nextcloud: | ||
networks: | networks: | ||
mein_netzwerk: | |||
ipv4_address: 172.18.0.20 # feste IP (optional) | ipv4_address: 172.18.0.20 # feste IP (optional) | ||
collabora: | collabora: | ||
networks: [ | networks: [mein_netzwerk] # Kurzschreibweise ohne feste IP | ||
networks: | networks: | ||
mein_netzwerk: | |||
external: true # Netzwerk existiert bereits | external: true # Netzwerk existiert bereits | ||
</ | </pre> | ||
Ohne <code>networks</code>-Angabe landet der Container im automatisch erstellten | Ohne <code>networks</code>-Angabe landet der Container im automatisch erstellten | ||
| Zeile 302: | Zeile 367: | ||
Überschreibt DNS — wird vor jeder DNS-Anfrage ausgewertet. | Überschreibt DNS — wird vor jeder DNS-Anfrage ausgewertet. | ||
< | <pre> | ||
services: | services: | ||
nextcloud: | nextcloud: | ||
extra_hosts: | extra_hosts: | ||
- "collabora.beispiel.de:172.18.0.6" | - "collabora.beispiel.de:172.18.0.6" | ||
</ | </pre> | ||
Typischer Anwendungsfall: Hairpinning vermeiden — ein Container soll eine Domain | Typischer Anwendungsfall: Hairpinning vermeiden — ein Container soll eine Domain | ||
| Zeile 317: | Zeile 382: | ||
Startreihenfolge und Abhängigkeiten zwischen Diensten. | Startreihenfolge und Abhängigkeiten zwischen Diensten. | ||
< | <pre> | ||
services: | services: | ||
nextcloud: | nextcloud: | ||
| Zeile 325: | Zeile 390: | ||
redis: | redis: | ||
condition: service_healthy | condition: service_healthy | ||
</ | </pre> | ||
{| class="wikitable" | {| class="wikitable" | ||
| Zeile 341: | Zeile 406: | ||
Prüft ob der Container wirklich funktioniert — nicht nur ob er läuft. | Prüft ob der Container wirklich funktioniert — nicht nur ob er läuft. | ||
< | <pre> | ||
services: | services: | ||
nc-db: | nc-db: | ||
| Zeile 350: | Zeile 415: | ||
retries: 30 # wie oft wiederholen bevor unhealthy | retries: 30 # wie oft wiederholen bevor unhealthy | ||
start_period: 30s # Anlaufzeit bevor Healthcheck zählt | start_period: 30s # Anlaufzeit bevor Healthcheck zählt | ||
</ | </pre> | ||
Das <code>$$</code> ist kein Tippfehler — doppeltes Dollar-Zeichen verhindert | Das <code>$$</code> ist kein Tippfehler — doppeltes Dollar-Zeichen verhindert | ||
dass Compose die Variable selbst auflöst; sie wird | dass Compose die Variable selbst auflöst; sie wird an die Shell im Container | ||
im Container übergeben. | übergeben. | ||
=== tmpfs === | === tmpfs === | ||
Flüchtige Dateisysteme die nur im RAM leben und beim Container-Stopp verschwinden. | Flüchtige Dateisysteme die nur im RAM leben und beim Container-Stopp verschwinden. | ||
< | <pre> | ||
services: | services: | ||
nc-redis: | nc-redis: | ||
| Zeile 373: | Zeile 437: | ||
- /tmp | - /tmp | ||
- /var/tmp | - /var/tmp | ||
</ | </pre> | ||
Redis im RAM-only-Modus: schneller, kein Disk-I/O, Daten gehen beim Neustart verloren — | Redis im RAM-only-Modus: schneller, kein Disk-I/O, Daten gehen beim Neustart verloren — | ||
| Zeile 382: | Zeile 446: | ||
DNS-Server die der Container verwenden soll statt des Docker-Defaults. | DNS-Server die der Container verwenden soll statt des Docker-Defaults. | ||
< | <pre> | ||
services: | services: | ||
nextcloud: | nextcloud: | ||
| Zeile 389: | Zeile 453: | ||
- 9.9.9.9 # Quad9 | - 9.9.9.9 # Quad9 | ||
- 8.8.8.8 # Google | - 8.8.8.8 # Google | ||
</ | </pre> | ||
=== command === | === command === | ||
| Zeile 395: | Zeile 459: | ||
Überschreibt den Standard-Startbefehl des Images. | Überschreibt den Standard-Startbefehl des Images. | ||
< | <pre> | ||
services: | services: | ||
nc-db: | nc-db: | ||
command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW | command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW | ||
</ | </pre> | ||
=== security_opt und cap_drop / cap_add === | === security_opt und cap_drop / cap_add === | ||
| Zeile 405: | Zeile 469: | ||
Container-Härtung: Linux-Capabilities entziehen oder hinzufügen. | Container-Härtung: Linux-Capabilities entziehen oder hinzufügen. | ||
< | <pre> | ||
services: | services: | ||
nc-redis: | nc-redis: | ||
| Zeile 413: | Zeile 477: | ||
- ALL # alle Capabilities entziehen | - ALL # alle Capabilities entziehen | ||
cap_add: | cap_add: | ||
- NET_BIND_SERVICE # nur diese eine wieder erlauben | - NET_BIND_SERVICE # nur diese eine wieder erlauben | ||
</ | </pre> | ||
Faustregel | Faustregel: | ||
* <code>no-new-privileges: true</code> — immer möglich, immer gut | * <code>no-new-privileges: true</code> — immer möglich, immer gut | ||
* <code>cap_drop: ALL</code> — wo immer es geht | * <code>cap_drop: ALL</code> — wo immer es geht | ||
| Zeile 426: | Zeile 490: | ||
Verzeichnisse die Schreibzugriff benötigen: | Verzeichnisse die Schreibzugriff benötigen: | ||
< | <pre> | ||
services: | services: | ||
nc-redis: | nc-redis: | ||
| Zeile 434: | Zeile 498: | ||
- /tmp | - /tmp | ||
- /run | - /run | ||
</ | </pre> | ||
---- | ---- | ||
| Zeile 442: | Zeile 506: | ||
=== Externes Netzwerk verwenden === | === Externes Netzwerk verwenden === | ||
< | <pre> | ||
networks: | networks: | ||
mein_netzwerk: | |||
external: true # Netzwerk wurde bereits mit docker network create angelegt | external: true # Netzwerk wurde bereits mit docker network create angelegt | ||
</ | </pre> | ||
=== Neues Netzwerk anlegen === | === Neues Netzwerk anlegen === | ||
< | <pre> | ||
networks: | networks: | ||
mein_netzwerk: | mein_netzwerk: | ||
| Zeile 458: | Zeile 522: | ||
- subnet: 172.20.0.0/16 | - subnet: 172.20.0.0/16 | ||
gateway: 172.20.0.1 | gateway: 172.20.0.1 | ||
</ | </pre> | ||
---- | ---- | ||
| Zeile 466: | Zeile 530: | ||
=== Benanntes Volume === | === Benanntes Volume === | ||
< | <pre> | ||
volumes: | volumes: | ||
caddy_data: # Docker verwaltet den Speicherort unter /var/lib/docker/volumes/ | caddy_data: # Docker verwaltet den Speicherort unter /var/lib/docker/volumes/ | ||
caddy_config: | caddy_config: | ||
</ | </pre> | ||
=== Volume mit tmpfs-Treiber (RAM-Disk) === | === Volume mit tmpfs-Treiber (RAM-Disk) === | ||
< | <pre> | ||
volumes: | volumes: | ||
cool-child-roots: | cool-child-roots: | ||
| Zeile 481: | Zeile 545: | ||
device: tmpfs | device: tmpfs | ||
o: "size=4g,mode=1777" | o: "size=4g,mode=1777" | ||
</ | </pre> | ||
---- | ---- | ||
| Zeile 498: | Zeile 562: | ||
| <code>docker compose restart caddy</code> || Nur einen Dienst neu starten | | <code>docker compose restart caddy</code> || Nur einen Dienst neu starten | ||
|- | |- | ||
| <code>docker compose up -d --force-recreate nextcloud</code> || Einen Container neu erstellen | | <code>docker compose up -d --force-recreate nextcloud</code> || Einen Container neu erstellen | ||
|- | |- | ||
| <code>docker compose logs -f nextcloud</code> || Log eines Dienstes live verfolgen | | <code>docker compose logs -f nextcloud</code> || Log eines Dienstes live verfolgen | ||
| Zeile 516: | Zeile 580: | ||
! Fehler !! Ursache !! Fix | ! Fehler !! Ursache !! Fix | ||
|- | |- | ||
| <code>yaml: line X: found character that cannot start any token</code> || Tab statt Leerzeichen | | <code>yaml: line X: found character that cannot start any token</code> || Tab statt Leerzeichen || Alle Tabs durch Leerzeichen ersetzen | ||
|- | |- | ||
| <code>Address already in use</code> || Feste IP bereits von anderem Container belegt || <code>docker network inspect</code> → Schuldigen finden | | <code>Address already in use</code> || Feste IP bereits von anderem Container belegt || <code>docker network inspect</code> → Schuldigen finden | ||
|- | |- | ||
| Container startet, | | Container startet, Dienst funktioniert nicht || <code>depends_on</code> ohne <code>condition: service_healthy</code> || Healthcheck ergänzen | ||
|- | |- | ||
| Umgebungsvariable leer | | Umgebungsvariable leer || <code>.env</code> liegt nicht im selben Verzeichnis || Pfad prüfen oder <code>env_file</code> explizit angeben | ||
|- | |- | ||
| <code>$$VARIABLE</code> wird nicht aufgelöst || Gewolltes Verhalten | | <code>$$VARIABLE</code> wird nicht aufgelöst || Gewolltes Verhalten || Für Shell-Variablen im Container: <code>$$</code>. Für Compose-Variablen: <code>$</code> | ||
|} | |} | ||
| Zeile 532: | Zeile 596: | ||
* [[Webserver, Reverse Proxy und Container – wie die Schichten zusammenspielen]] | * [[Webserver, Reverse Proxy und Container – wie die Schichten zusammenspielen]] | ||
* [[Docker-Netzwerke und Container-IPs | * [[Docker-Netzwerke und Container-IPs]] | ||
* [[Hairpin-NAT – Collabora und Nextcloud hinter Caddy]] | * [[Hairpin-NAT – Collabora und Nextcloud hinter Caddy]] | ||
* [https://docs.docker.com/compose/compose-file/ Offizielle Compose-Referenz (englisch)] | * [https://docs.docker.com/compose/compose-file/ Offizielle Compose-Referenz (englisch)] | ||
[[Kategorie:Docker]] | [[Kategorie:Docker]] | ||
Aktuelle Version vom 24. April 2026, 12:20 Uhr
YAML — Die Auszeichnungssprache in der Docker-Compose-Welt
YAML steht für „YAML Ain't Markup Language" — ein rekursives Akronym, typisch für die Open-Source-Welt. Es ist eine Sprache zur Darstellung strukturierter Daten, die für Menschen lesbar sein soll.
Geschichte
YAML wurde 2001 von drei Entwicklern entworfen: Clark Evans, Ingy döt Net und Oren Ben-Kiki. Das ursprüngliche Akronym lautete noch „Yet Another Markup Language" — in Anlehnung an die vielen XML-Varianten der Zeit. Es wurde bald zu „YAML Ain't Markup Language" umgedeutet, um klarzustellen: das hier ist keine Dokumentensprache wie HTML oder XML, sondern eine reine Datensprache.
Das Grundproblem das YAML lösen wollte war real: XML war für Konfigurationsdateien viel zu geschwätzig — jeder Wert brauchte ein öffnendes und schließendes Tag. JSON existierte noch gar nicht (das kam erst 2002). Man wollte etwas das Menschen wirklich lesen und schreiben können, ohne ständig spitze Klammern tippen zu müssen.
Verbreitung vor Docker
Docker hat YAML nicht erfunden — es hat YAML einer neuen Generation bekannt gemacht. Vorher war YAML in anderen Welten zuhause:
- Ruby on Rails (2004) — der erste große Durchbruch. Rails verwendete YAML
für Datenbankverbindungen, Übersetzungen und Konfiguration. Eine ganze Generation von Webentwicklern lernte YAML über Rails, lange vor Docker.
- Ansible (2012) — das Konfigurationsmanagement-Tool wählte YAML als zentrale
Sprache für Playbooks. Für Systemadministratoren war Ansible der eigentliche YAML-Einstieg.
- Kubernetes (2014) — parallel zu Docker entstanden, ebenfalls YAML für alles.
Kubernetes hat YAML in der Infrastrukturwelt zementiert.
- CI/CD-Systeme — GitHub Actions, GitLab CI, Travis CI, CircleCI — alle
verwenden YAML für Pipeline-Definitionen.
- OpenAPI/Swagger — API-Dokumentation wird seit Jahren in YAML geschrieben.
Docker Compose hat YAML dann für Heimserver-Betreiber und Administratoren ohne Rails- oder Ansible-Hintergrund zum Standard gemacht.
Kritik in der Fachwelt
YAML hat einen schlechten Ruf in Teilen der Entwicklergemeinde — nicht wegen der Grundidee, sondern wegen der Implementierung.
Die Spezifikation ist notorisch komplex: YAML 1.2 umfasst über 80 Seiten. Verschiedene Parser verhalten sich bei Grenzfällen unterschiedlich. Und es gibt berühmte Stolperfallen:
# Diese Werte sind KEIN String — YAML interpretiert sie als Boolean: land: no # → false antwort: yes # → true aktiv: on # → true # Das hier ist kein String — es wird zur Zahl: version: 1.0 # → Float 1.0, nicht String "1.0" port: 8080 # → Integer
Das bekannteste Beispiel ist das sogenannte Norway Problem: das Länderkürzel
NO wird in älteren YAML-Parsern automatisch zu false
— was dazu geführt hat dass norwegische Entwickler in Konfigurationsdateien
ihren eigenen Ländercode nicht als String verwenden konnten ohne Anführungszeichen.🤣
YAML 1.2 (2009) hat viele dieser Probleme behoben, aber ältere Parser sind noch weit verbreitet.
Das Fazit vieler erfahrener Entwickler: YAML ist für Menschen gut lesbar, aber für Programme schwer korrekt zu implementieren. Für Konfigurationsdateien die selten geändert werden ist es hervorragend — für Programme die YAML generieren oder parsen müssen, ist es eine Quelle von Überraschungen.
YAML und JSON — Verwandtschaft, kein Dialekt
Der häufige Vergleich „YAML ist ein JSON-Dialekt" ist ungenau. Die präzise Formulierung: YAML ist eine Obermenge von JSON.
- Jedes gültige JSON ist auch gültiges YAML.
- Umgekehrt gilt das nicht: YAML kann Dinge ausdrücken die JSON nicht kann
(Kommentare, mehrzeilige Strings, Anker und Referenzen).
- Die Syntax ist völlig unterschiedlich: JSON verwendet geschweifte Klammern
und Anführungszeichen, YAML verwendet Einrückung und Doppelpunkte.
Dasselbe Datenkonstrukt in beiden Sprachen:
JSON:
{
"services": {
"nextcloud": {
"image": "nextcloud:latest",
"restart": "unless-stopped"
}
}
}
YAML:
services:
nextcloud:
image: nextcloud:latest
restart: unless-stopped
YAML ist lesbarer — auf Kosten einer strengen Regel: Einrückung mit Leerzeichen, niemals mit Tabs. Ein falsch gesetzter Tab ist der häufigste Fehler in compose-Dateien.
Kommentare
YAML unterstützt Kommentare mit # — JSON nicht.
Das macht YAML deutlich besser geeignet für Konfigurationsdateien die Menschen
pflegen:
# Dieser Container darf keinen direkten Außenzugriff haben ports: [] # keine ports → kein direkter Zugriff von außen
Datentypen
YAML erkennt Datentypen automatisch:
| Wert | Typ | Anmerkung |
|---|---|---|
true / false |
Boolean | Ohne Anführungszeichen |
42 |
Integer | |
3.14 |
Float | |
hallo |
String | Ohne Anführungszeichen wenn eindeutig |
"true" |
String | Mit Anführungszeichen erzwungen |
null |
Null |
Stolperfalle: restart: always ist ein String.
restart: true wäre ein Boolean — und ungültig.
Im Zweifel Anführungszeichen setzen.
Grundaufbau einer docker-compose.yml
Eine docker-compose.yml besteht aus maximal vier Abschnitten
auf oberster Ebene:
services: # Pflicht — definiert die Container ... networks: # Optional — definiert Netzwerke ... volumes: # Optional — definiert persistente Datenspeicher ... configs: # Optional — definiert Konfigurationsdateien ...
Der services-Abschnitt ist der einzig verpflichtende.
Alles andere ist optional und wird nur angegeben wenn benötigt.
Einrückungsregeln
Die Hierarchie wird ausschließlich durch Einrückung ausgedrückt. Zwei Leerzeichen pro Ebene sind Konvention:
services: # Ebene 1
nextcloud: # Ebene 2 — Name des Dienstes
image: ... # Ebene 3 — Direktive des Dienstes
networks: # Ebene 3 — Direktive (Liste folgt)
- tuxi_net # Ebene 4 — Listenelement
Direktiven im services-Abschnitt
image
Das Docker-Image das für den Container verwendet wird.
services:
nextcloud:
image: lscr.io/linuxserver/nextcloud:latest
Format: registry/image:tag. Ohne Registry wird Docker Hub angenommen.
Ohne Tag wird latest angenommen — aber explizit angeben ist besser
für Reproduzierbarkeit.
container_name
Fester Name des Containers. Ohne diese Direktive generiert Docker Compose einen Namen aus Projektname + Servicename + Nummer.
services:
nextcloud:
container_name: nextcloud
Wichtig: der Container-Name ist gleichzeitig der Docker-interne DNS-Name mit dem andere Container diesen Dienst ansprechen können.
restart
Neustart-Verhalten des Containers.
restart: unless-stopped
| Wert | Verhalten |
|---|---|
no |
Nie neu starten (Standard) |
always |
Immer neu starten, auch nach manuellem Stopp |
unless-stopped |
Neu starten außer wenn manuell gestoppt — empfohlen für Produktionsdienste |
on-failure |
Nur neu starten wenn der Container mit Fehlercode endet |
image vs. build
Statt ein fertiges Image zu verwenden kann Docker Compose auch selbst eines bauen:
services:
mein-dienst:
build:
context: ./mein-verzeichnis # Pfad zum Dockerfile
dockerfile: Dockerfile.prod # optional: anderer Dateiname
ports
Portweiterleitung zwischen Host und Container: HOST:CONTAINER.
services:
caddy:
ports:
- "80:80"
- "443:443"
- "443:443/udp" # für HTTP/3
Wichtig: Wer ports weglässt, ist nur im Docker-Netzwerk erreichbar —
nicht von außen. Das ist für interne Dienste wie Datenbanken oder Collabora gewünscht.
volumes
Einbinden von Verzeichnissen oder benannten Volumes in den Container.
services:
nextcloud:
volumes:
- ./nextcloud/app:/config # Bind Mount: Host-Pfad → Container-Pfad
- /data/nextcloud-data:/data # absoluter Host-Pfad
- caddy_data:/data # benanntes Volume (unten definiert)
volumes:
caddy_data: # Docker verwaltet den Speicherort
| Typ | Syntax | Wann verwenden |
|---|---|---|
| Bind Mount | ./host/pfad:/container/pfad |
Wenn du direkten Zugriff auf die Dateien brauchst |
| Benanntes Volume | volume_name:/container/pfad |
Für Datenbankdaten, Caches — Docker verwaltet den Speicherort |
| tmpfs | Siehe tmpfs-Direktive |
Flüchtige Daten die nur im RAM leben sollen |
environment
Umgebungsvariablen die in den Container übergeben werden.
services:
nextcloud:
environment:
- PUID=1000
- PGID=1001
- TZ=Europe/Berlin
- MYSQL_PASSWORD=${MYSQL_PASSWORD} # Wert aus .env-Datei
Alternativ als Map-Schreibweise:
environment:
PUID: "1000"
TZ: Europe/Berlin
env_file
Umgebungsvariablen aus einer externen Datei laden — sinnvoll für Passwörter:
services:
nc-db:
env_file:
- .env
Die .env-Datei enthält dann:
MYSQL_ROOT_PASSWORD=sehrgeheim MYSQL_DATABASE=nextcloud MYSQL_USER=ncuser MYSQL_PASSWORD=auchgeheim
Die .env-Datei gehört in .gitignore — niemals in ein
öffentliches Repository einchecken.
networks
Netzwerkzugehörigkeit des Containers.
services:
nextcloud:
networks:
mein_netzwerk:
ipv4_address: 172.18.0.20 # feste IP (optional)
collabora:
networks: [mein_netzwerk] # Kurzschreibweise ohne feste IP
networks:
mein_netzwerk:
external: true # Netzwerk existiert bereits
Ohne networks-Angabe landet der Container im automatisch erstellten
Default-Netzwerk des Projekts — Container in verschiedenen Projekten können sich
dann nicht erreichen.
extra_hosts
Einträge die direkt in die /etc/hosts des Containers geschrieben werden.
Überschreibt DNS — wird vor jeder DNS-Anfrage ausgewertet.
services:
nextcloud:
extra_hosts:
- "collabora.beispiel.de:172.18.0.6"
Typischer Anwendungsfall: Hairpinning vermeiden — ein Container soll eine Domain intern auflösen statt den Umweg über die externe IP zu nehmen. Siehe Hairpin-NAT – Collabora und Nextcloud hinter Caddy.
depends_on
Startreihenfolge und Abhängigkeiten zwischen Diensten.
services:
nextcloud:
depends_on:
db:
condition: service_healthy # wartet bis Healthcheck grün ist
redis:
condition: service_healthy
| condition | Bedeutung |
|---|---|
service_started |
Container wurde gestartet (Standard — prüft nicht ob er wirklich läuft) |
service_healthy |
Healthcheck des abhängigen Dienstes ist grün |
service_completed_successfully |
Abhängiger Dienst hat sich mit Exit-Code 0 beendet |
healthcheck
Prüft ob der Container wirklich funktioniert — nicht nur ob er läuft.
services:
nc-db:
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u root -p$$MYSQL_ROOT_PASSWORD || exit 1"]
interval: 10s # wie oft prüfen
timeout: 5s # wie lange auf Antwort warten
retries: 30 # wie oft wiederholen bevor unhealthy
start_period: 30s # Anlaufzeit bevor Healthcheck zählt
Das $$ ist kein Tippfehler — doppeltes Dollar-Zeichen verhindert
dass Compose die Variable selbst auflöst; sie wird an die Shell im Container
übergeben.
tmpfs
Flüchtige Dateisysteme die nur im RAM leben und beim Container-Stopp verschwinden.
services:
nc-redis:
tmpfs:
- /data
- /tmp
- /run
collabora:
tmpfs:
- /tmp
- /var/tmp
Redis im RAM-only-Modus: schneller, kein Disk-I/O, Daten gehen beim Neustart verloren — für Session-Caches gewünscht.
dns
DNS-Server die der Container verwenden soll statt des Docker-Defaults.
services:
nextcloud:
dns:
- 1.1.1.1 # Cloudflare
- 9.9.9.9 # Quad9
- 8.8.8.8 # Google
command
Überschreibt den Standard-Startbefehl des Images.
services:
nc-db:
command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
security_opt und cap_drop / cap_add
Container-Härtung: Linux-Capabilities entziehen oder hinzufügen.
services:
nc-redis:
security_opt:
- no-new-privileges:true # verhindert Privilege Escalation
cap_drop:
- ALL # alle Capabilities entziehen
cap_add:
- NET_BIND_SERVICE # nur diese eine wieder erlauben
Faustregel:
no-new-privileges: true— immer möglich, immer gutcap_drop: ALL— wo immer es gehtread_only: true— nur bei schlanken Services ohne viele Schreibpfade
read_only
Container-Dateisystem schreibgeschützt — kombiniert mit tmpfs für
Verzeichnisse die Schreibzugriff benötigen:
services:
nc-redis:
read_only: true
tmpfs:
- /data
- /tmp
- /run
Der networks-Abschnitt
Externes Netzwerk verwenden
networks:
mein_netzwerk:
external: true # Netzwerk wurde bereits mit docker network create angelegt
Neues Netzwerk anlegen
networks:
mein_netzwerk:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
gateway: 172.20.0.1
Der volumes-Abschnitt
Benanntes Volume
volumes: caddy_data: # Docker verwaltet den Speicherort unter /var/lib/docker/volumes/ caddy_config:
Volume mit tmpfs-Treiber (RAM-Disk)
volumes:
cool-child-roots:
driver_opts:
type: tmpfs
device: tmpfs
o: "size=4g,mode=1777"
Nützliche CLI-Befehle
| Befehl | Was er tut |
|---|---|
docker compose up -d |
Alle Dienste im Hintergrund starten |
docker compose down |
Alle Dienste stoppen und Container entfernen |
docker compose restart |
Alle Dienste neu starten |
docker compose restart caddy |
Nur einen Dienst neu starten |
docker compose up -d --force-recreate nextcloud |
Einen Container neu erstellen |
docker compose logs -f nextcloud |
Log eines Dienstes live verfolgen |
docker compose ps |
Status aller Dienste anzeigen |
docker compose exec nextcloud bash |
Shell in laufendem Container öffnen |
docker compose pull |
Alle Images aktualisieren |
Häufige Fehler
| Fehler | Ursache | Fix |
|---|---|---|
yaml: line X: found character that cannot start any token |
Tab statt Leerzeichen | Alle Tabs durch Leerzeichen ersetzen |
Address already in use |
Feste IP bereits von anderem Container belegt | docker network inspect → Schuldigen finden
|
| Container startet, Dienst funktioniert nicht | depends_on ohne condition: service_healthy |
Healthcheck ergänzen |
| Umgebungsvariable leer | .env liegt nicht im selben Verzeichnis |
Pfad prüfen oder env_file explizit angeben
|
$$VARIABLE wird nicht aufgelöst |
Gewolltes Verhalten | Für Shell-Variablen im Container: $$. Für Compose-Variablen: $
|
