Type coercion: the biggest silent bomb
YAML infers scalar types automatically, and many values that "look like strings" get silently converted: no/na/n become the boolean false (the famous Norway problem), on/off/yes become true/false, 0733 becomes octal, and 1.10 becomes the number 1.1.
The result is config that "looks right but runs wrong": version numbers lose precision, country codes turn into booleans. Unless you explicitly want a number/boolean/null, quote every scalar — the single most stress-reducing YAML rule.
country: no # Actually false, not Norway!
country: "no" # This is a string
version: 1.10 # Becomes the number 1.1
version: "1.10" # String, safeWhen quoting is mandatory
Three classes of values must be quoted: (1) anything that could be coerced to a number/boolean/null (no, on, 1.10, plus 2026-01-01, which parses as a date object); (2) values starting with special characters (* & ! % @); (3) values containing "colon + space" or #.
The date trap deserves emphasis: date: 2026-01-01 is a date object in most parsers, not a string, and it surfaces in a totally different shape during log debugging. Write date: "2026-01-01" for a string.
date: 2026-01-01 # Date object, not a string
date: "2026-01-01" # String
value: a: b # Syntax error (colon + space)
value: "a: b" # CorrectIndentation: one level off, data lands elsewhere
YAML accepts spaces only, never tabs (a tab is a hard syntax error); sibling keys must align; content after a list marker - belongs to a child level. One level off and data silently lands under the wrong node — most parsers never complain.
Two frequent details: a space must follow the colon (key:value is invalid), and a comment # needs a preceding space or it becomes part of the value. A YAML language server (e.g. Red Hat YAML for VSCode) flags these live.
server:
host: 0.0.0.0 # Indented 2 spaces, belongs to server
ports:
- 80 # List items indent one more level
- 443
# key:value is wrong → key: value is rightMultiline strings: | vs >
The pipe | keeps newlines (literal block); the fold > collapses newlines into spaces (folded block); adding - strips the trailing newline (|-, >-). Use | for scripts, certificates and SQL; use > for long prose.
The indentation baseline is set by the first line: every line in the block must be indented deeper than the parent key and consistently — this is exactly why "it looks aligned but still errors": you compared against the wrong level.
script: |
#!/usr/bin/env bash
set -euo pipefail
echo "ok"
desc: >
This is a long block of description text,
where newlines are folded into spaces.
sql: |-
SELECT *
FROM users;Anchors, aliases and merge keys
An anchor &name marks a node, an alias *name references it, and <<: merges a mapping into the current key — this is how docker-compose reuses shared config.
Two limits to remember: lists cannot be merged with <<, only referenced whole; an alias is a reference, not a copy, so changing one place changes all. Bonus trap: unquoted 80:80 parses as a sexagesimal (base-60) number under YAML 1.1 — always quote port mappings as "80:80" in compose files.
common: &common
image: nginx:1.27
restart: unless-stopped
web:
<<: *common
ports:
- "80:80"Debugging toolchain and checklist
Validate before shipping: yamllint for style and structure, the consuming tool itself (docker compose config, kubectl --dry-run), or a one-liner yaml.safe_load in Python to confirm what the parser actually sees.
Debugging principle: the reported line usually marks where the problem surfaced, while the real mistake sits a few lines above in indentation or quoting. Checklist: quote scalars, spaces only, space after the colon, pick the right |/> block, quote port mappings, run a parser before committing.
yamllint config.yaml
docker compose config # Parse and output normalized config
kubectl apply --dry-run=client -f manifest.yaml
python -c "import yaml,sys; yaml.safe_load(open(sys.argv[1]))" config.yaml