Skip to content
Latchkey LogoLatchkey home

Docker Compose: the attribute version is obsolete, in CI

Docker compose the attribute version is obsolete is a deprecation warning and not an error: on the run we recorded, Compose printed it and still exited 0. The step went red because of the gate wrapped around the command, so there are two things to settle here, one line in the compose file and one decision about what your pipeline counts as a failure.

Runner log: Compose warns about the version key, exits zero, and a stderr gate fails
The recorded run: the warning on stderr, Compose exit code 0 on the next line, and the step failing underneath it because the script read stderr as failure.
Diagram of the warning path, the exit code, and the gate that turns one into a failure
Compose ignores the key and reports it. The failure is added by the step around it, which is why deleting the key fixes today and not the next deprecation.

What this error means

A Compose step turns red while Compose itself is content. The line names your file and then says the attribute version is obsolete, it will be ignored, please remove it to avoid potential confusion. The shape around that text depends on where it runs: our recorded run on a runner printed it as a log line beginning with a timestamp and a warning level, and a developer machine with a terminal attached prints the same sentence behind a WARN prefix, which is the form bassmaster187/TeslaLogger#1347 quotes. In the recorded run the command exited 0 with the warning on stderr, and the step failed only because the script treated any stderr output as a failure.

Actions log, docker compose config, Compose v5.5.0
time="2026-09-20T10:55:49Z" level=warning msg="/home/runner/compose-version-demo/compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion"
--- compose exit code: 0
::error::docker compose wrote to stderr

Reproduced on a Latchkey runner

Run 2026-09-20·Runner latchkey-small·Exit code 1

Docker version 29.7.2, build a7dcaa6
Docker Compose version v5.5.0
--- the file
version: "3.8"
services:
  web:
    image: alpine:3
--- docker compose config -q, with stderr captured and its own exit code recorded
time="2026-09-20T10:55:49Z" level=warning msg="/home/runner/compose-version-demo/compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion"
--- compose exit code: 0
--- the same command behind a step that treats any stderr as a failure
::error::docker compose wrote to stderr
[latchkey-bash-wrapper] BEGIN sidecar POST (boot_wait=30s max_time=320s url=http://localhost/diagnose socket=/run/latchkey-self-heal/sock)
[latchkey-bash-wrapper] END sidecar POST ok (attempts=1 http=200)

The runner diagnosed the failure and did not retry it; this failure needs the fix below.

Compose warns, your pipeline fails

The Compose specification still accepts a top-level version key. The reference we read on 2026-09-20 says the property is defined for backward compatibility, that it is only informative, and that you will receive a warning message that it is obsolete if used. The same page says Compose always uses the most recent schema to validate the file regardless of the version field, which is the part worth keeping: the key selects nothing.

So deleting it changes nothing about your services. What it changes is the stderr output of every Compose command in the job, and stderr is what your pipeline was reacting to.

LayerWhat it does with versionWhat our run recorded
Compose reads the fileIgnores it, validates against the current schemaWarning on stderr
Compose finishes the commandPrints the merged config as askedExit code 0
A step that fails on stderrTurns the warning into a failed stepExit code 1
A schema linter in the jobMay reject the key outrightDepends on the linter

Common causes

A top-level version key left over from Compose v1

The common one. The key was required years ago, it survived every copy of the file since, and Compose v2 onwards reports it on every command. It is not doing anything: the documentation states the property is informative only and that validation uses the current schema whatever the value says.

A step that treats anything on stderr as a failure

This is what actually fails the job, and it is worth naming precisely because the fix belongs in the workflow. Our recorded run captured stderr, printed the Compose exit code 0 on the next line, and then exited 1 from the gate. In our experience the gate is usually inherited from a shared CI template rather than written for this repository.

A generator or template that keeps writing the key

Scaffolding tools, older internal generators and copied service templates still emit version: "3.8" at the top. Delete it in one repository and the next generated service brings it back, which is why the grep in fix three matters more than the edit in fix one.

A schema validator that rejects the key outright

Some linting steps validate the compose file against a strict schema of their own rather than asking Compose. Those can fail on an obsolete key rather than warn about it, which produces a genuinely failing command instead of a warning, with a different message. If your log has no Compose warning at all, look for one of these.

How to fix it

Delete the version line

  1. Remove the top-level version key from every compose file in the repository.
  2. Leave everything else alone: services, networks and volumes are unaffected.
  3. Run docker compose config once to confirm stderr is now empty.
Terminal
grep -rl '^version:' --include='*compose*.y*ml' .
sed -i '/^version:/d' compose.yaml
docker compose config -q 2>&1 >/dev/null | wc -c   # expect 0

Gate on the exit code, not on stderr

Commands write progress, deprecations and hints to stderr, and treating that as failure makes every future deprecation a build break. Let the command decide, and keep the output in the log where a human can read it.

.github/workflows/ci.yml
# not this
[ -z "$(docker compose config -q 2>&1 >/dev/null)" ] || exit 1
# this
docker compose config -q

Fix the template that keeps writing it

Search the whole organization, not just the failing repository. A generator that emits the key will reintroduce it in the next service, and the second time around nobody remembers why the gate was removed.

Terminal
grep -rn '^version:' --include='*compose*.y*ml' . | head

If a gate must read stderr, let it ignore known deprecations

Some teams genuinely want stderr to be quiet, for example when a step is expected to print nothing at all. Filter the lines you have already triaged rather than failing on everything, and keep the filter small enough that a new message still gets your attention.

.github/workflows/ci.yml
ERR=$(docker compose config -q 2>&1 >/dev/null | grep -v 'is obsolete' || true)
[ -z "$ERR" ] || { echo "$ERR"; exit 1; }

Find the gate before you rewrite the file

Deleting the key fixes today. The gate stays, and the next deprecation Docker adds lands in exactly the same place, which is why it is worth spending two minutes finding out which shape you have. In our experience it is one of three: a step that captures stderr and fails when it is not empty, a command piped into a grep under set -o pipefail, or an internal wrapper script that every repository inherited and nobody owns.

The tell is in the log. If the Compose command printed its normal output and the failure line came from your own script, the gate is yours. If the failure line comes from an action you did not write, look at that action before you look at the compose file.

.github/workflows/ci.yml
# the three shapes, in the order we meet them
ERR=$(docker compose config -q 2>&1 >/dev/null)
[ -z "$ERR" ] || exit 1          # 1: any stderr is failure

set -o pipefail
docker compose up -d 2>&1 | grep -qv WARN   # 2: a grep decides

./ci/compose-check.sh            # 3: the wrapper nobody owns

Delete the key, and check what else was reading it

The fix in the file is one line, and the file below is the whole of what Compose needs: services, and the services you want. Nothing replaces the version key, because nothing was using it.

If a tool in your pipeline parses the compose file itself and expects a version, that tool is what pins you to a v1-era schema, not the file. Pin the fix there rather than keeping a key whose only remaining effect is a warning on every command in the job.

compose.yaml
# before
version: "3.8"
services:
  web:
    image: alpine:3

# after
services:
  web:
    image: alpine:3

What the runner does about it

Nothing, and nothing is the right answer. On the recorded run the wrapper posted the failure to the self-heal sidecar and the sidecar answered, no repair followed, and the job ended on the exit code our own gate produced. Latchkey carries no pattern for this, and it should not: the command succeeded, and no change to the environment makes a pipeline stop treating warnings as failures.

How to prevent it

  • Keep compose files at the current schema: no top-level version, anywhere.
  • Gate CI steps on exit codes, and keep stderr for reading.
  • Fix the generator, not only the file it generated.
  • Print the command exit code next to its output in any step you expect to argue about.

Frequently asked questions

Do I still need version in docker-compose.yml?
No. The Compose reference we read on 2026-09-20 says the top-level version property exists for backward compatibility, is only informative, and produces the obsolete warning when used. It also says Compose validates against the most recent schema regardless of the value, so the key cannot pin a schema and nothing replaces it when you delete it.
Does the obsolete version warning break anything?
Not on its own. On our recorded run the command printed the warning on stderr and exited 0, and the services in the file were parsed normally. It breaks a job only where something in the pipeline treats stderr output, rather than an exit code, as the definition of failure.
Why does my CI fail on a Docker Compose warning?
Because a step around the command is reading stderr. The usual shapes are a script that captures stderr and fails when it is not empty, a pipeline under set -o pipefail whose grep decides the result, or a shared wrapper script inherited from a CI template. The log tells you which: if the failure line is your own text, the gate is yours.
How do I remove the obsolete version warning from docker compose?
Delete the top-level version line from every compose file, then run docker compose config once and confirm stderr is empty. Search the whole repository, including files named for an environment such as a staging or test override, because Compose reads each file you pass and warns for each one that still carries the key.

Related guides

References

Not every red build is a broken build. Latchkey runs your Actions at $0.0025/min at 2 vCPU. Start free → 30-day trial · No credit card