docker/cli · error
top-level object must be a mapping
Error message
top-level object must be a mapping
What it means
Thrown by ParseYAML after it unmarshals the YAML bytes into a generic any and asserts the result is a map[string]any. Compose files must be a mapping at the top level (e.g. keyed by 'services:'); a YAML document whose root is a list, a scalar, or empty does not satisfy this and is rejected before any further parsing. This guards the contract that a compose file is an object of named sections, not a bare array or value.
Solutions
- Open the offending compose file and ensure the top-level is a mapping, e.g. start with 'services:' as the root key.
- If you intended a list of services, restructure it as 'services:\n name: {...}' so the list becomes named service entries.
- Run 'docker compose config' on the file to reproduce and get the exact file path in the error.
- Check that interpolation did not strip the entire document; ensure variables referenced by the top-level keys resolve.
Example fix
// before (broken - root is a list)
- web:
image: nginx
// after
services:
web:
image: nginx Defensive patterns
Strategy: validation
Validate before calling
// Validate a compose document is a top-level mapping before calling loader.Load.
func isTopLevelMapping(b []byte) error {
var v any
if err := yaml.Unmarshal(b, &v); err != nil {
return fmt.Errorf("invalid yaml: %w", err)
}
if v == nil {
return errors.New("compose file is empty")
}
if _, ok := v.(map[string]any); !ok {
return errors.New("top-level yaml is not a mapping; expected keys like 'services:'")
}
return nil
} Type guard
func isMappingDoc(b []byte) bool {
var v any
_ = yaml.Unmarshal(b, &v)
_, ok := v.(map[string]any)
return ok
} Prevention
- Validate the file shape with 'docker compose config' before committing.
- Never submit per-service snippets as standalone compose files.
- Lint compose files in CI with a yaml parser asserting the root is an object.
When it happens
Trigger: Calling loader.ParseYAML or loader.Load with a compose file whose first non-comment line is '- ' (a sequence) or a plain scalar like 'hello', or with a file that is empty/contains only comments after interpolation. Passing a fragment intended to be merged under an existing key rather than a full document also triggers it.
Common situations: A developer splits a compose file and accidentally submits a per-service snippet (a single map under a service name) as a standalone file. A YAML list of services produced by a templating tool or AI assistant. A file reduced to only comments or whitespace after variable substitution removed all content.
Related errors
- specify a Compose file (with --compose-file)
- no files specified
- invalid mount config for type "bind": field Source must not…
- compose file contains unsupported options
- invalid image reference for service
AI-assisted analysis of docker/cli@4f84911bfe (2026-08-07).
Data as JSON: /api/errors/4b6f3790a135aa22.
Report an issue: GitHub.
Appendix: source
Thrown at cli/compose/loader/loader.go:69
return volumespec.Parse(spec)
}
// WithDiscardEnvFiles sets the Options to discard the `env_file` section after resolving to
// the `environment` section
func WithDiscardEnvFiles(options *Options) {
options.discardEnvFiles = true
}
// ParseYAML reads the bytes from a file, parses the bytes into a mapping
// structure, and returns it.
func ParseYAML(source []byte) (map[string]any, error) {
var cfg any
if err := yaml.Unmarshal(source, &cfg); err != nil {
return nil, err
}
_, ok := cfg.(map[string]any)
if !ok {
return nil, errors.New("top-level object must be a mapping")
}
converted, err := convertToStringKeysRecursive(cfg, "")
if err != nil {
return nil, err
}
return converted.(map[string]any), nil
}
// Load reads a ConfigDetails and returns a fully loaded configuration
func Load(configDetails types.ConfigDetails, opt ...func(*Options)) (*types.Config, error) {
if len(configDetails.ConfigFiles) < 1 {
return nil, errors.New("no files specified")
}
options := &Options{
Interpolate: &interp.Options{
Substitute: template.Substitute,
LookupValue: configDetails.LookupEnv,View on GitHub (pinned to 4f84911bfe)