How to Fix Docker Compose 'yaml: unmarshal errors' Invalid Indentation in compose.yaml
Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 5–15 mins
TL;DR
- What broke:
docker-compose config(ordocker compose config) is aborting withyaml: unmarshal errorsbecause the YAML parser hit structurally invalid indentation — mixed tabs/spaces, wrong nesting depth, or a key at an illegal indent level. - How to fix it: Enforce 2-space indentation throughout, eliminate all tab characters, and validate nesting hierarchy against the Compose spec.
- Fast path: Use our Client-Side Sandbox above to auto-refactor this — paste your broken compose.yaml and it will pinpoint the offending lines and emit corrected output without sending your config anywhere.
The Incident (What Does the Error Mean?)
Raw error output from a broken compose.yaml:
$ docker compose config
yaml: unmarshal errors:
line 14: cannot unmarshal !!str `ports` into compose.ServiceConfig
line 22: field volumes not found in type compose.ServiceConfig
ERROR: The Compose file './compose.yaml' is invalid because:
services.api.image must be a string
The Go YAML parser (gopkg.in/yaml.v3) is strict. When it encounters a key at an indentation level it doesn't expect — because a tab was used instead of spaces, or a block was indented 3 spaces instead of 2 — it misidentifies the key's parent node. ports gets parsed as a string value of the previous key instead of a sibling mapping. Everything downstream of that line is misread. docker compose up, docker compose run, and any CI pipeline calling docker compose config as a lint gate will hard-fail.
The Attack Vector / Blast Radius
This is not a security vulnerability in the traditional sense, but the blast radius in a CI/CD pipeline is severe:
- Full deployment gate failure. Any pipeline using
docker compose configas a validation step will exit non-zero. Deployments stop. - Silent misconfiguration risk. If your editor auto-corrects the YAML just enough to parse without error but the nesting is semantically wrong (e.g., an
environmentblock attaches to the wrong service), you deploy with incorrect env vars — wrong DB hosts, wrong secrets references, wrong resource limits — without a parse error ever being thrown. - Volume mount misreads are the worst case: a malformed
volumesblock that parses as a string instead of a mapping silently drops the mount definition. Your container starts with no persistent storage. Data loss on first write. - Multi-developer drift. One engineer uses VSCode with tab expansion, another uses vim with
noexpandtab. The file becomes a mix of tabs and spaces. YAML spec (section 6.1) forbids tabs as indentation. The parser will reject the file on any strict-mode toolchain.
How to Fix It (The Solution)
Basic Fix: Strip Tabs, Enforce 2-Space Indentation
Run this before anything else:
# Detect tab characters in your compose file
cat -A compose.yaml | grep -P "^\t"
# Convert all tabs to 2 spaces (GNU sed)
sed -i 's/\t/ /g' compose.yaml
# Validate immediately
docker compose config
Root Cause Example — Broken vs. Fixed
services:
api:
image: myapp:latest
ports:
- - "8080:8080" # TAB-indented — parser reads this as string scalar
+ - "8080:8080" # 6 spaces: 2 (services) + 2 (api) + 2 (list item)
environment:
- DATABASE_URL: postgres://db:5432/app # 6 spaces — correct
- SECRET_KEY: supersecret # 8 spaces — WRONG, misaligned sibling
+ DATABASE_URL: postgres://db://db:5432/app
+ SECRET_KEY: supersecret # 6 spaces — correct sibling
volumes:
- - ./data:/var/lib/app/data # 4 spaces — wrong nesting under service
+ - ./data:/var/lib/app/data # 6 spaces — correct
Enterprise Best Practice: Schema Validation + Editor Enforcement
1. Use docker compose config as a hard lint gate — never skip it.
# Makefile
- deploy:
- docker compose up -d
+ lint:
+ docker compose config > /dev/null
+ deploy: lint
+ docker compose up -d
2. Enforce YAML formatting via .editorconfig at the repo root:
+ [*.yaml]
+ indent_style = space
+ indent_size = 2
+ tab_width = 2
+ trim_trailing_whitespace = true
+ insert_final_newline = true
3. Add yamllint with a strict config:
# .yamllint.yml
extends: default
rules:
indentation:
spaces: 2
indent-sequences: true
check-multi-line-strings: true
truthy:
allowed-values: ['true', 'false']
pip install yamllint
yamllint -c .yamllint.yml compose.yaml
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
GitHub Actions — Gate on Every PR
# .github/workflows/compose-lint.yml
name: Validate Compose
on: [pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: yamllint
run: |
pip install yamllint
yamllint -c .yamllint.yml compose.yaml
- name: docker compose config
run: docker compose config > /dev/null
Pre-commit Hook (Local Enforcement)
# .pre-commit-config.yaml
repos:
- repo: https://github.com/adrienverge/yamllint
rev: v1.35.1
hooks:
- id: yamllint
args: ['-c', '.yamllint.yml']
- repo: https://github.com/IamTheFij/docker-pre-commit
rev: v3.0.1
hooks:
- id: docker-compose-check
Checkov for Compose Security + Syntax
pip install checkov
checkov -f compose.yaml --framework dockerfile
Checkov will catch indentation-caused misconfigurations and flag security issues like privileged: true or missing read-only root filesystems in the same pass.
The rule: docker compose config must return exit code 0 before any image build or deployment step is permitted to run. No exceptions.