YAML: Die Auszeichnungssprache in der Docker-Compose-Welt
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: $
|
