# Docker Compose: the attribute version is obsolete, in CI

> Fix docker compose the attribute version is obsolete in GitHub Actions: Compose only warns and exits 0, so the failure is the gate around the step.

Source: https://latchkey.dev/learn/docker/docker-compose-version-attribute-obsolete-error-in-ci  
Updated: 2026-09-20

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.

## 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
```

## 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; }
```

## 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.

## 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.

| Layer | What it does with `version` | What our run recorded |
| --- | --- | --- |
| Compose reads the file | Ignores it, validates against the current schema | Warning on stderr |
| Compose finishes the command | Prints the merged config as asked | Exit code 0 |
| A step that fails on stderr | Turns the warning into a failed step | Exit code 1 |
| A schema linter in the job | May reject the key outright | Depends on the linter |

> The first two rows are from our own run on a `latchkey-small` runner on 2026-09-20, Compose v5.5.0. The wording is quoted from the Compose version and name reference, read the same day.

## 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.

## FAQ

### 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.

## References

- [Docker docs: Compose version and name top-level elements](https://docs.docker.com/reference/compose-file/version-and-name/)
- [Docker docs: the Compose file reference](https://docs.docker.com/reference/compose-file/)
- [GitHub docs: workflow commands, including the error annotation](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions)
- [bassmaster187/TeslaLogger#1347: the warning as a terminal prints it](https://github.com/bassmaster187/TeslaLogger/issues/1347)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
