What this error means
The message yaml: did not find expected key is a YAML parsing error. Docker Compose could not interpret your docker-compose.yml because of malformed YAML. This is usually due to indentation, missing colons, stray characters, or invalid list/map structure—not Docker-specific logic.
Quickstart: fix it fast
- Validate syntax:
- Run:
This prints the normalized config or fails with a clearer location.docker compose config
- Run:
- Show line numbers when opening the file so the reported line is obvious.
- Replace tabs with spaces (YAML forbids tabs for indentation):
grep -n $'\t' docker-compose.yml || true expand -t 2 docker-compose.yml > /tmp/compose.yml && mv /tmp/compose.yml docker-compose.yml - Ensure every mapping key has a colon and value (e.g., image: nginx:1.25).
- Check lists under ports, volumes, environment, command for correct indentation.
- Quote values containing colons, hash (#), or special characters.
- Re-run docker compose config until it succeeds.
Minimal working example
Use this to compare structure and spacing (2 spaces per indent, no tabs):
services:
web:
image: nginx:1.25
ports:
- "8080:80"
environment:
- NGINX_HOST=localhost
- NGINX_PORT=80
volumes:
- ./html:/usr/share/nginx/html:ro
command: ["nginx", "-g", "daemon off;"]
Notes:
- No top-level version key is required in modern Compose.
- Quotes around "8080:80" avoid ambiguity with the colon.
Common causes and fixes
| Symptom | Example (bad) | Fix |
|---|---|---|
| Tabs used for indent | Tabs before keys | Replace tabs with spaces (2 or 4 spaces consistently) |
| Missing colon after key | image nginx:1.25 | image: nginx:1.25 |
| Misindented list items | ports:\n- "8080:80" | ports:\n - "8080:80" |
| Unquoted value with colon | command: python -m http.server:8000 | command: "python -m http.server:8000" or list form |
| Dangling dash | environment:\n - | Remove the dangling dash or provide a value |
| Mixed list/map styles | environment: NGINX_HOST=localhost | Use list or map, not a bare string |
| Inline comments in the wrong place | image: nginx:1.25 #tag: latest | Keep comments away from ambiguous colons or quote the value |
Indentation and tabs
Bad:
services:
\tweb:
image: nginx:1.25
Good:
services:
web:
image: nginx:1.25
Missing colons
Bad:
services:
web
image: nginx:1.25
Good:
services:
web:
image: nginx:1.25
Lists under ports/volumes/environment
Bad:
services:
api:
image: my/api
ports:
- "8080:80"
Good:
services:
api:
image: my/api
ports:
- "8080:80"
Values with colons or special characters
Bad (plain scalar with colon can confuse parser depending on context):
services:
app:
command: python -m http.server:8000
Good (quote or use list):
services:
app:
command: "python -m http.server:8000"
# or
services:
app:
command: ["python", "-m", "http.server", "8000"]
environment syntax
All of these are valid; mixing them incorrectly is not.
Map style:
environment:
APP_ENV: production
LOG_LEVEL: info
List style:
environment:
- APP_ENV=production
- LOG_LEVEL=info
Bad (bare string):
environment: APP_ENV=production
Multiline commands and entrypoints
Use YAML block scalars or arrays, with correct indentation.
Bad:
command:
- |
npm run start
--port 3000
Good:
command: |
npm run start \
--port 3000
# or
command: ["npm", "run", "start", "--", "--port", "3000"]
Top-level sections ordering
Compose expects known top-level keys (services, networks, volumes). Unknown keys or misplacement won’t typically cause this exact error, but bad indentation around them will. Keep services at the root and define networks/volumes at the root as needed.
Step-by-step debugging procedure
- Run docker compose config to get a precise error location.
- Open the file with visible whitespace; replace tabs with spaces.
- Validate YAML structure:
- Top-level: services
- Next level: service names (web, api)
- Under each service: keys like image, build, ports, environment, volumes
- Check each list:
- All list items start with - aligned under their parent key.
- No trailing dashes.
- Check each mapping:
- Every key has a colon and a value, even if empty (key: "").
- Quote risky scalars:
- Anything with :, #, {, }, [, ], ,, &, *, ?, |, >, @, or leading/trailing spaces.
- Minimize to isolate:
- Comment out sections until the error disappears, then focus on the last change.
- Rebuild the normalized config:
docker compose -f docker-compose.yml config - If using multiple files, test them individually, then merge:
docker compose -f docker-compose.yml -f docker-compose.override.yml config
Performance notes (compose workflows)
- Keep build contexts small using .dockerignore to speed up docker compose build.
- Prefer multi-stage builds to reduce image size and startup time.
- Avoid unnecessary bind mounts of large directories; mount only what you need.
- Use named volumes for dependency caches (e.g., package managers) to speed rebuilds.
- Limit service logs during development (e.g., logging options) to reduce I/O.
- Use depends_on only where needed; unnecessary service startups slow feedback loops.
Tiny FAQ
Why does docker compose config help? It parses and renders your YAML, catching syntax errors and showing the merged result across multiple files.
Do I need a version key? Not in modern Compose. The Compose Specification does not require version; services at the root are enough.
Can comments break YAML? Comments starting with # are fine, but if you put a colon-like pattern inside an unquoted value next to a comment, quoting the value is safer.
Does indentation size matter? Any consistent number of spaces works. Two spaces per level is common; tabs are not allowed.
How do I find tabs quickly? Use grep -n $'\t' docker-compose.yml and replace with spaces.