Initializing Enclave...

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 (or docker compose config) is aborting with yaml: unmarshal errors because 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 config as 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 environment block 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 volumes block 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.

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →