Docker Compose JSON: Schema, docker compose config --format json, and Inspecting State

Last updated:

Compose files are usually YAML, but the Compose Specification is format-agnostic — JSON is a fully supported input format, and JSON is the native output format for docker compose config --format json, docker compose ps --format json, and docker inspect. This guide covers the JSON side of Compose v2: the published JSON Schema at the compose-spec repo, how to produce the resolved JSON view of a project, how to script against ps output, and how to use jq with inspect JSON for Compose-managed containers. Compose v2 (docker compose, not the legacy docker-compose Python tool) is assumed throughout — v1 has been end-of-life since 2023.

Working with a Compose JSON file or pasting docker inspect output into a ticket? Run it through Jsonic's JSON Validator first — it pinpoints trailing commas, broken quotes, and bracket mismatches at exact line numbers, which is faster than re-running docker compose config on a CI box.

Validate Compose JSON

Can a Docker Compose file be JSON? (yes — but here is the trade-off)

The Compose Specification defines a data model — services, networks, volumes, configs, secrets, and the relationships between them. It does not define a file syntax. Both YAML and JSON serialize the same model, and the Compose CLI accepts either: compose.json, docker-compose.json, or any -f path/to/file.json argument all work the same way as their YAML counterparts.

{
  "services": {
    "web": {
      "image": "nginx:1.27-alpine",
      "ports": ["8080:80"],
      "environment": {
        "TZ": "UTC"
      },
      "depends_on": ["redis"],
      "restart": "unless-stopped"
    },
    "redis": {
      "image": "redis:7-alpine",
      "volumes": ["redis-data:/data"]
    }
  },
  "volumes": {
    "redis-data": {}
  }
}

The equivalent YAML — same model, friendlier to write:

services:
  web:
    image: nginx:1.27-alpine
    ports: ["8080:80"]
    environment:
      TZ: UTC
    depends_on: [redis]
    restart: unless-stopped
  redis:
    image: redis:7-alpine
    volumes:
      - redis-data:/data
volumes:
  redis-data: {}

When JSON wins: when another tool generates the file (a Terraform provider, a templating script, a UI). Emitting JSON is easier than emitting valid YAML with correct indentation, and JSON has no surprising type coercions (the YAML 1.1Norway problem where no becomes false, for example). When YAML wins: hand-authored config. YAML has comments, anchors and aliases, multi-line strings, and less quoting noise — practical for any file a human edits regularly. For more on the formats, see YAML vs JSON.

The Compose Specification JSON Schema

The Compose Specification publishes a JSON Schema at github.com/compose-spec/compose-spec in the schemas/ directory. The file is compose-spec.json, drafted against JSON Schema draft-07. It defines every top-level key — services, networks, volumes, configs, secrets, include, name — and the allowed shape of each value all the way down to individual service fields like healthcheck, deploy.resources, and logging.driver.

{
  "$schema": "https://json-schema.org/draft-07/schema#",
  "id": "compose-spec.json",
  "type": "object",
  "properties": {
    "version": { "type": "string" },
    "name": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_-]*$" },
    "services": {
      "type": "object",
      "patternProperties": {
        "^[a-zA-Z0-9._-]+$": { "$ref": "#/definitions/service" }
      }
    },
    "networks": { "type": "object" },
    "volumes": { "type": "object" }
  }
}

IDE support builds on this schema. VS Code with the Docker extension reads it and provides autocomplete on image, restart, ports, and every other key. JetBrains IDEs do the same. Adding a $schema reference at the top of a JSON-format Compose file makes editor validation explicit:

{
  "$schema": "https://raw.githubusercontent.com/compose-spec/compose-spec/main/schemas/compose-spec.json",
  "services": { "web": { "image": "nginx" } }
}

The schema is the same regardless of input format — YAML or JSON. After parsing, both produce identical in-memory structures, so the same schema validates both. For the broader pattern, see JSON Schema validation.

docker compose config --format json: rendering YAML to JSON

docker compose config is the workhorse for producing JSON from any Compose project, regardless of whether the source files are YAML or JSON. Without flags it prints the resolved configuration as YAML; with --format json it emits a single JSON document on stdout. The flag was added in Compose v2.6 and is stable in all current releases.

The resolved qualifier is the important part. The output reflects everything Compose computed before deploying:

  • Override files merged (compose.override.yml, -f file2.yml stacks)
  • Environment variables interpolated (${TAG:-latest} becomes the actual value)
  • extends directives expanded into the referenced service's fields
  • include directives pulled in
  • Defaults filled in (project name, default network, default driver settings)
# Generate resolved JSON
docker compose config --format json > resolved.json

# Inline with jq
docker compose config --format json | jq '.services | keys'

# Diff against the previous deploy's resolved config
git show HEAD~1:ci/resolved.json > prev.json
diff <(docker compose config --format json | jq -S .) \
     <(jq -S . prev.json)

A trimmed example of what the output looks like:

{
  "name": "myproject",
  "services": {
    "web": {
      "image": "nginx:1.27-alpine",
      "networks": { "default": null },
      "ports": [
        { "mode": "ingress", "target": 80, "published": "8080", "protocol": "tcp" }
      ],
      "environment": { "TZ": "UTC" },
      "depends_on": { "redis": { "condition": "service_started", "required": true } },
      "restart": "unless-stopped"
    }
  },
  "networks": { "default": { "name": "myproject_default" } },
  "volumes": { "redis-data": { "name": "myproject_redis-data" } }
}

Note how compact shorthand in the source (ports: ["8080:80"]) expands into the canonical long form (mode, target, published, protocol) in the resolved output. That canonicalization is part of why the JSON view is useful for diffing and CI — every equivalent input collapses to the same output.

docker compose ps --format json for scripting

docker compose ps lists the containers a project owns. The default output is a human table; --format json returns a JSON array (one object per container) ideal for scripting and CI checks. The fields cover the running state, ports, and basic identity — exactly what you need to verify a deploy.

$ docker compose ps --format json
[
  {
    "Name": "myproject-web-1",
    "Image": "nginx:1.27-alpine",
    "Service": "web",
    "State": "running",
    "Status": "Up 5 minutes (healthy)",
    "Health": "healthy",
    "ExitCode": 0,
    "Publishers": [
      { "URL": "0.0.0.0", "TargetPort": 80, "PublishedPort": 8080, "Protocol": "tcp" }
    ],
    "Networks": ["myproject_default"]
  },
  {
    "Name": "myproject-redis-1",
    "Image": "redis:7-alpine",
    "Service": "redis",
    "State": "running",
    "Status": "Up 5 minutes",
    "Health": "",
    "ExitCode": 0
  }
]

The --format flag also accepts Go template syntax. The most common alternative is --format '{{json .}}', which emits one JSON object per line (NDJSON) instead of a single array — easier to stream and easier to process line-by-line in shell pipelines.

# List names of running containers
docker compose ps --format json | jq -r '.[] | select(.State == "running") | .Name'

# Print service:health pairs
docker compose ps --format json | jq -r '.[] | "\(.Service): \(.Health // "n/a")"'

# Exit non-zero if any service is not running (CI gate)
docker compose ps --format json \
  | jq -e 'all(.[]; .State == "running")' > /dev/null

# NDJSON variant — one object per line
docker compose ps --format '{{json .}}' | while read -r line; do
  echo "$line" | jq -r '.Service'
done

The Health field is populated only when the service defines a healthcheck block; otherwise it is an empty string. The ExitCode field is meaningful only after a container has exited — for running containers it is 0. For deeper jq patterns, see our jq filter examples.

Parsing docker inspect JSON with jq for Compose containers

docker inspect returns the full low-level state of containers, networks, and volumes as JSON. Compose-managed objects are no different from manually created ones — the inspect output is the same — but the project label com.docker.compose.project and service label com.docker.compose.service let you filter by Compose membership.

The shape that surprises people: docker inspect always returns a JSON array, even for a single target. That makes scripting predictable but requires either indexing (jq '.[0]') or the --format flag to unwrap.

# Inspect a Compose container by service name
docker inspect $(docker compose ps -q web)

# Same, unwrapped to a single object via jq
docker inspect $(docker compose ps -q web) | jq '.[0]'

# Extract just the health status as a string
docker inspect --format '{{.State.Health.Status}}' \
  $(docker compose ps -q web)

# Extract environment variables as JSON
docker inspect $(docker compose ps -q web) \
  | jq '.[0].Config.Env'

# Find all Compose projects on this host
docker ps --format json \
  | jq -r '.Labels | split(",") | map(select(startswith("com.docker.compose.project="))) | .[]' \
  | sort -u

The path you most often want inside the inspect JSON:

jq pathWhat it returns
.[0].State.Statusrunning, exited, restarting, paused
.[0].State.Health.Statusstarting, healthy, unhealthy (only if healthcheck defined)
.[0].Config.EnvArray of KEY=value strings
.[0].NetworkSettings.NetworksObject keyed by network name, with IP and aliases
.[0].MountsArray of volume and bind mounts
.[0].Config.LabelsAll labels, including com.docker.compose.*

For more on JSON usage with Docker outside of Compose — daemon.json, registry config, and raw inspect — see JSON in Docker (daemon.json, inspect).

Compose extends and JSON anchors equivalent

YAML's anchor (&name) and alias (*name) syntax lets you tag a node and reuse it elsewhere — useful for sharing a logging config or a set of labels across many services. JSON has no equivalent syntax. The Compose-specific replacement is the extends key, which works in both YAML and JSON files and expresses the same intent at the service level.

{
  "services": {
    "base-worker": {
      "image": "myorg/worker:1.4",
      "restart": "unless-stopped",
      "logging": {
        "driver": "json-file",
        "options": { "max-size": "10m", "max-file": "3" }
      },
      "environment": { "LOG_LEVEL": "info" }
    },
    "queue-worker": {
      "extends": { "service": "base-worker" },
      "command": ["worker", "--queue", "default"]
    },
    "cron-worker": {
      "extends": { "service": "base-worker" },
      "command": ["worker", "--queue", "cron"],
      "environment": { "LOG_LEVEL": "debug" }
    }
  }
}

The two child services inherit image, restart, logging, and the base environment from base-worker, then add or override specific fields. cron-worker overrides LOG_LEVEL to debug — merge semantics: scalars and lists replace, maps merge key-by-key.

extends can also reference a service in a different file — { "file": "common.json", "service": "base-worker" } — which is the JSON way to share configuration across multiple projects. The drawback compared to YAML anchors is that extends only works at the service level, not for arbitrary fragments (you cannot share just a logging block). For sharing smaller pieces, generate the JSON from a templating script that expands the shared blocks.

include is the related directive for pulling in entire services from another file — different shape (the included file is merged at the project level), same goal (avoid duplication across compose files).

JSON output for CI: parsing deploy status programmatically

CI pipelines benefit from JSON output because exit-code-only checks are too coarse — a green build that deployed an image with the wrong tag is still wrong. The pattern is: render the resolved config to JSON, deploy, then verify the actual state with ps --format json and inspect.

#!/usr/bin/env bash
set -euo pipefail

# 1. Render resolved config and store as a CI artifact
docker compose config --format json > artifacts/resolved.json

# 2. Verify the resolved image tags match what we expected
EXPECTED_TAG="$(git rev-parse --short HEAD)"
ACTUAL=$(jq -r '.services.web.image' artifacts/resolved.json)
case "$ACTUAL" in
  *":$EXPECTED_TAG") ;;
  *) echo "image tag mismatch: $ACTUAL" >&2; exit 1 ;;
esac

# 3. Deploy
docker compose up -d --wait

# 4. Verify every service is running and healthy
docker compose ps --format json > artifacts/ps.json
jq -e 'all(.[]; .State == "running" and (.Health == "" or .Health == "healthy"))' \
   artifacts/ps.json > /dev/null

# 5. Capture full inspect output for postmortem visibility
docker inspect $(docker compose ps -q) > artifacts/inspect.json

The --wait flag on up makes Compose block until every service is running and (if a healthcheck is defined) healthy, then exits non-zero if any service failed to come up. Combined with the JSON verification step, you get both fast failure (--wait returns immediately on healthcheck failure) and rich diagnostics (the JSON artifacts capture exactly what state the cluster was in).

For idempotent CI deploys where the same JSON could be replayed without side effects, the same techniques used for HTTP APIs apply at the Compose level — see JSON Validator for ensuring the JSON artifacts themselves are well-formed before publishing them.

Compose JSON vs Kubernetes manifest JSON: when to switch

Compose JSON and Kubernetes manifest JSON look similar at first glance — both describe containerized workloads in a declarative document — but they target different runtimes and have different ergonomics. Compose is a single-host (or single Swarm cluster) tool with a project-scoped model; Kubernetes is a multi-host orchestrator with namespaces, controllers, and a much larger surface area.

ConcernCompose JSONKubernetes JSON manifests
ScopeOne project per file (services + networks + volumes)One resource per document (Deployment, Service, ConfigMap, etc.)
Multi-hostSwarm mode adds it, but rarely used todayNative — that is the entire point
Schema sourcecompose-spec.json (one schema covers everything)Per-resource OpenAPI schemas published by the API server
Resolved viewdocker compose config --format jsonkubectl get -o json after apply
Health modelContainer-level healthcheck in service definitionLiveness, readiness, startup probes on Pods
Update strategyStop and replace (rolling in Swarm mode)Rolling, recreate, blue-green via Deployments
Best forLocal dev, single-server prod, CI environmentsMulti-server prod, complex scaling, GitOps

When to migrate: when you need horizontal scaling across hosts, rolling updates with traffic shifting, secrets rotation as a first-class operation, or any of the controller-based patterns (Jobs, CronJobs, StatefulSets). Compose works at small scale; Kubernetes pays off when the operational model needs to.

When to stay on Compose: single-host deployments, local development environments, CI test fixtures, and any setup where the operational complexity of Kubernetes (control plane, etcd, ingress controllers, RBAC) outweighs the benefits. Tools like kompose can convert Compose JSON to Kubernetes JSON manifests when you do migrate — the field names overlap enough that the conversion is mostly mechanical. For the Kubernetes side, see Kubernetes JSON manifests.

Key terms

Compose Specification
The open spec at compose-spec.io that defines the data model Docker Compose v2 and other tools (Podman Compose, Nerdctl) implement. Format-agnostic — same model in YAML or JSON.
resolved configuration
The fully merged, interpolated, defaulted view of a Compose project — what Compose actually deploys. Produced by docker compose config --format json.
--format json
A flag on most docker compose subcommands (config, ps, images, top) that switches output from a human table to machine-readable JSON. Added in Compose v2.6.
{{json .}}
Go template syntax accepted by Docker's --format flag. Emits one JSON object per line (NDJSON) instead of a single JSON array — convenient for shell streaming.
extends
A service-level field that inherits configuration from another service (in the same file or a different one). The JSON-compatible replacement for YAML anchors and aliases.
compose-spec.json
The JSON Schema file at github.com/compose-spec/compose-spec that defines every Compose field. Drives IDE autocomplete and standalone validation.
Docker Compose v2
The current Compose CLI, invoked as docker compose (with a space). Implemented in Go as a Docker CLI plugin. Replaces the legacy hyphenated docker-compose Python tool that has been end-of-life since 2023.

Frequently asked questions

Can I write a docker-compose file in JSON instead of YAML?

Yes. The Compose Specification is format-agnostic — it defines a data model, not a file syntax. Docker Compose v2 accepts JSON input wherever it accepts YAML; you can name the file compose.json, docker-compose.json, or pass any JSON file with -f. The trade-off is ergonomic: JSON has no comments, no anchors, no multi-line strings, and no native env interpolation hints (you still get ${VAR} substitution from Compose itself, but writing it in JSON is verbose). The case where JSON wins is generated config — when another tool produces the Compose document, emitting JSON is easier than emitting valid YAML with correct indentation. The case where YAML wins is hand-authored config: anchors and aliases, comments explaining why a service has a specific restart policy, and the absence of quoting noise on strings make YAML the practical default for humans.

How do I output the resolved Compose configuration as JSON?

Run docker compose config --format json from the directory containing your compose file. Compose merges any override files (compose.override.yml), substitutes environment variables, resolves extends and include directives, and emits the fully resolved configuration as a single JSON document on stdout. This is the canonical view of what Compose will actually deploy — the same model the engine consumes after parsing. Redirect to a file with > resolved.json to capture it for CI artifacts or audit. The --format json flag was added in Compose v2.6 and is stable as of Compose v2.x. For older v1 docker-compose (the Python one), the equivalent was docker-compose config --format json but the schema differed slightly; if you are still on v1, the upgrade to v2 is overdue — v1 has been end-of-life since 2023.

How do I parse docker compose ps output as JSON?

Use docker compose ps --format json. The output is a JSON array (one object per service container) with fields like Name, Image, Service, State, Status, Health, ExitCode, Publishers, and Networks. Pipe it through jq to extract specific fields: docker compose ps --format json | jq -r ".[] | select(.State == \"running\") | .Name" lists the names of running containers. The --format flag also accepts Go template syntax — docker compose ps --format "{{json .}}" emits one JSON object per line (NDJSON), which is more convenient than a single array when streaming or feeding into tools like jq --slurp or awk. For health-check status specifically, the Health field is populated only when the service defines a healthcheck — otherwise it is an empty string.

What is the JSON Schema for docker-compose.yml?

The Compose Specification publishes a JSON Schema at github.com/compose-spec/compose-spec in the schemas/ directory (compose-spec.json). It defines every top-level key (services, networks, volumes, configs, secrets, include, extends), every service-level field, and the allowed shape of each value. The schema is what powers IDE autocompletion in VS Code (via the Docker extension) and validation in tools like docker compose config (which validates implicitly) and external validators like ajv or python-jsonschema. Note that the schema is the same regardless of input format — whether you write the file in YAML or JSON, the data model is identical, so the same schema validates both after parsing. The Compose CLI bundles its own version of the schema; the published one tracks the spec and may be slightly ahead of any specific Compose release.

How do I validate a Compose file against the JSON Schema?

The simplest validation is docker compose config — it parses your file, applies overrides, and exits non-zero with an error message if anything fails the spec. For explicit JSON Schema validation outside Compose, convert the YAML to JSON first (docker compose config --format json > resolved.json), then run a standalone validator against the compose-spec schema: ajv validate -s compose-spec.json -d resolved.json. This catches type errors and unknown fields that Compose itself sometimes accepts permissively. For CI, the docker compose config exit code is usually enough — it is fast, requires no extra tooling, and rejects the same files Compose would reject at deploy time. Schema-level validation matters when you author Compose files in another tool and want to verify them without invoking Docker (in a linter, for example).

Can I use anchors and aliases in JSON like in YAML?

No — JSON has no anchor or alias syntax. The YAML feature where you tag a node with &default and reuse it elsewhere with *default has no direct JSON equivalent. The Compose-specific replacement is the extends key, which lets one service inherit from another (in the same file or a different one) and override specific fields. extends works in both YAML and JSON Compose files, and it expresses the same intent — share configuration without duplicating it — just at the service level rather than at the arbitrary-node level YAML anchors allow. For sharing deeper fragments (a logging config used by ten services), define the fragment as a service template (a service that no one runs but others extend from) or generate the JSON programmatically from a script that expands the shared pieces.

Why does docker inspect output JSON arrays even for one container?

docker inspect always returns a JSON array because it accepts multiple targets. Even when you pass a single container name, the output is a one-element array — that uniform shape makes scripting predictable. To extract the single object, use docker inspect <name> | jq ".[0]" or the built-in --format flag with Go template syntax: docker inspect --format "{{json .}}" <name> emits one object per target as NDJSON instead of a single array. For Compose containers specifically, you can combine compose ps and inspect: docker inspect $(docker compose ps -q service-name) returns the inspect JSON for whichever container is currently running that service. The --format "{{.State.Health.Status}}" pattern extracts a single field as a plain string, which is what you usually want in shell scripts.

How do I extract a single field from docker compose config --format json?

Pipe to jq. To list every service name: docker compose config --format json | jq -r ".services | keys[]". To get the image for one service: docker compose config --format json | jq -r ".services.web.image". To extract every published port across all services: docker compose config --format json | jq -r ".services | to_entries[] | .value.ports[]?.published". jq pays off because the resolved JSON includes everything Compose computed — interpolated env vars, merged overrides, resolved extends — so you are querying the final state, not the source file. For non-jq environments (CI runners without jq), python -c "import json,sys; print(json.load(sys.stdin)[\"services\"][\"web\"][\"image\"])" works equivalently. The resolved JSON shape closely tracks the Compose Specification, so the same paths work across projects.

Further reading and primary sources