If you have ever spun up a stack using docker compose up only to have the parser spit back the error non-string key at top level: 404, don't worry: your Docker Engine is perfectly fine. You are just dealing with a simple syntax error in your YAML file.
While YAML is a very practical format, it is extremely picky about spacing and data types. This specific error indicates that the parser encountered a key at the root (top level) of the document that is not a valid text string. According to the Compose specification, all root-level keys (version, services, volumes, networks) and service names must be evaluated strictly as text strings.
1. YAML parsing vs. schema validation: the core distinctions#
To effectively debug any Docker Compose file error, it is essential to understand that loading a compose file happens in two distinct operational phases:
- YAML Parsing Phase: The interpreter (such as the PyYAML library in Python or gopkg.in/yaml in Go) parses the raw text and converts it into native program structures (dictionaries, lists, integers, booleans). If there are misaligned keys, unbalanced brackets, or unquoted numeric keys at the root level, execution crashes here.
- Typical Error:
non-string key at top level: 404(the parser encountered an integer/object instead of a string key at the document root).
- Schema Validation Phase: Once the YAML structure is loaded into memory, Docker Compose validates it against the official Compose Spec (e.g., checking if ports follow correct patterns, if required properties exist, or if data types match).
- Typical Error:
services.web.ports must be a list of strings(the YAML was successfully parsed, but the structured values do not match the expected type).
The flowchart below illustrates this distinction:
2. Common causes of parsing and schema errors#
In addition to incorrect indentation, several data types can mislead the YAML parser if they are not explicitly declared as strings:
A. Port mappings declared without quotes (the classic case)#
In YAML, unquoted numeric structures or port sequences confuse the interpreter. There are two common scenarios for this error:
# ❌ INCORRECT (implicit mapping without hyphen)
ports:
404:404 # YAML interprets this as a dictionary {404: 404} with a numeric key
# Schema Error: services.app.ports must be a list of strings
# ❌ INCORRECT (list item without quotes)
ports:
- 404:404 # The parser attempts to resolve this as {404: 404} and fails, expecting a string
# Schema Error: services.app.ports[0] must be a string, not a number
# ✅ CORRECT
ports:
- "404:404" # Evaluated strictly as a text string
B. Compound and complex ports#
Any port mapping variation that includes specific IPs or ranges must be wrapped in quotes:
ports:
- "8080:80" # External port mapped to internal port
- "80:80" # Simple port mapping
- "127.0.0.1:8080:80" # Specific IP address binding
- "8080-8090:80-90" # Port range mapping
C. Difference between ports and expose#
Docker Compose uses distinct directives for public hosting and internal routing:
ports: Publishes ports to the host system (requires quotes due to YAML parsing rules).expose: Only exposes ports internally to other containers within the same virtual network. While less prone to syntax errors since it lacks colons (:), it should also be written as a string.
services:
app:
ports:
- "8080:80" # External mapping to the host (always use quotes)
expose:
- "80" # Private internal communication (always use quotes)
D. Special YAML types: booleans, nulls, and dates#
The YAML parser implicitly converts certain reserved keywords into native types. Attempting to pass these values without quotes in environment variables, volumes, or labels will trigger validation schema errors:
# ❌ INCORRECT
environment:
DEBUG: true # Interpreted as a raw boolean
# Schema Error: services.app.environment.DEBUG must be a string, not a boolean
labels:
cache: null # Interpreted as a raw null value
# Schema Error: services.app.labels.cache must be a string, not null
volumes:
2026-06-15: /data # Interpreted as a raw date object
# YAML Parse or Schema Error: non-string key / invalid date type
# ✅ CORRECT
environment:
DEBUG: "true" # Pure string
labels:
cache: "null" # Pure string
volumes:
"2026-06-15": /data # Pure string
E. Bad indentation pushing keys back to the root#
YAML is unforgiving with spaces. If you misalign a key like ports or even a service parameter, it can accidentally drift to the far-left margin, making the parser think it belongs to the root tree:
# ❌ INCORRECT
services:
app:
image: node:18
ports: # <-- Incorrect indentation shifts ports back to the document root
- "80:80"
# Schema Error: additional property ports is not allowed
services:
app:
image: node:18
404:404: # <-- Unquoted and at the root level
- "80:80"
# YAML Parse Error: non-string key at top level: 404
Because the Compose parser scans the file hierarchy, it hits invalid root-level keys and fails instantly because it has no service context there.
Keep your indentations perfectly aligned using spaces (never use tabs):
# ✅ CORRECT
services:
app:
image: node:18
ports:
- "80:80"
F. Deprecation of the version field#
In modern Docker Compose releases (Compose v2 / unified specification), the top-level version directive has been deprecated and is now completely optional. Keeping it may generate warnings. The current best practice is to remove it entirely from your file structure:
# ✅ CORRECT (No version tag - Valid in Compose v2)
services:
web:
image: nginx:alpine
ports:
- "80:80"
Validation and debugging tools#
Whenever you run into syntax errors, you can use several tools to find the root cause and automate error prevention:
1. Complete debugging with docker compose config#
This command validates your compose file configuration against the official JSON schema without spinning up any containers.
- If there is an error, it points out the exact line and schema mismatch:
$ docker compose config
# Typical error output:
services.web.ports must be a list of strings
services.web.ports[0] must be a string, not 404:404
- To inspect how the parser consolidates and interprets your syntax, view the parsed configuration using the
--format jsonflag:
docker compose config --format json
2. Local YAML linters#
To prevent pushing invalid configurations to your repositories, run a command-line linter locally:
# Install the YAML command-line linter
pip install yamllint
# Run the checks on your file
yamllint docker-compose.yml
For VS Code, install the Red Hat YAML extension, which provides real-time schema validation and autocompletion as you type.
3. CI/CD pipeline automation#
Integrate syntax validation directly into your continuous integration workflow (such as GitHub Actions) to catch invalid files before they reach production:
# .github/workflows/validate-compose.yml
name: Validate Docker Compose
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Validate Compose Syntax
run: docker compose config
- name: Run YAML Lint
run: |
pip install yamllint
yamllint docker-compose.yml
Was this article helpful?
Leave a quick reaction to help prioritize future technical guides:
This post is licensed under CC BY-NC.



Comments
Join the discussion below.
0 comments