invalid compose project is one of the least helpful-looking error suffixes in Docker Compose — it never says what's actually wrong, just tacks itself onto the end of a message like service "web" refers to undefined network backend: invalid compose project. That's not a coincidence or a vague catch-all: it's a single hardcoded sentinel string, reused verbatim across roughly twenty completely unrelated validation failures. This post traces it to the exact line of source that produces it, lists every distinct thing known to trigger it, and — just as usefully — lists the close look-alike errors that don't carry this suffix, so you're not searching for the wrong problem.
It's a generic sentinel, not a specific bug
Docker Compose v2 parses docker-compose.yml using compose-go, the reference library Docker maintains for loading Compose files. Right in its errdefs package sits this:
// ErrInvalid is returned when a compose project is invalid
ErrInvalid = errors.New("invalid compose project")That's the entire definition — one generic sentinel error, with no detail of its own. The real information lives in loader/validate.go, in a function called checkConsistency, which runs after your YAML has already parsed successfully. It walks the fully-resolved project — every service, network, volume, secret, config — checking that everything they reference actually exists and that no two settings contradict each other. Every failure it finds gets wrapped the same way:
return fmt.Errorf("service %q refers to undefined network %s: %w", s.Name, network, errdefs.ErrInvalid)Go's %w wraps the specific message onto the generic sentinel, and printing that wrapped error is exactly what produces "X: invalid compose project" on your terminal. The suffix is always identical because it's always the same sentinel value — which means it carries zero diagnostic information on its own. The entire fix is always in the text before the colon.
The most common triggers
These four account for the overwhelming majority of real reports of this error, and Common Docker Compose Errors walks through each with a full worked example — the short version:
- A service with no
image,build, orextends. Compose has nothing to actually run.service "web" has neither an image nor a build context specified: invalid compose project. Easy to hit mid-refactor, when animage:line gets deleted while switching a service to build locally, without addingbuild:in the same edit. - A
networks:entry that isn't declared at the top level. Every network a service lists (other than the implicitdefault) needs a matching entry under the top-levelnetworks:key. - A
volumes:entry that isn't declared at the top level. Same shape as the network case — a mount source that doesn't look like a path (no./,/, or~prefix) is treated as a named volume reference, which needs its own top-levelvolumes:entry. depends_onpointing at a service that doesn't exist — almost always a typo or a renamed/deleted service thatdepends_onstill references.
The rest of the list — verified straight from source
Beyond those four, checkConsistency covers about fifteen more distinct checks. Every one of these is confirmed, by reading the actual source, to wrap its error with the same invalid compose project sentinel:
| What triggers it | Fix |
|---|---|
A service lists a secrets:, configs:, or build secrets: entry that isn't declared at the top level | Add the matching top-level declaration, or fix the typo'd reference. |
A service lists a models: entry (Docker Model Runner) that isn't declared at the top level | Same shape as secrets/configs — declare it at the top level. |
Both network_mode and networks: set on the same service | Pick one — network_mode (e.g. host, service:another) attaches directly to an existing network namespace and bypasses Compose's own network management entirely, so it's incompatible with also joining named networks. |
Both build.dockerfile and build.dockerfile_inline set | Keep whichever one you actually intend — an external file or an inline string. |
build.platforms is set but doesn't include the service's own platform value | Add the service's platform to the build.platforms list. |
cpus / mem_limit / mem_reservation / pids_limit set to a different value than the matching deploy.resources field | These are two different-generation ways of writing the same limit — the short top-level field, and the Swarm-style deploy.resources.limits/reservations block. Compose allows either, but not two disagreeing values for the same thing. Pick one and delete the other. |
scale set to a different value than deploy.replicas | Same conflict, same fix — keep one, delete the other. |
A fixed container_name on a service also scaled above 1 replica (via scale: or deploy.replicas) | Container names have to be unique — remove container_name and let Compose generate one per replica, or drop back to a single replica. |
A develop.watch entry with no target, for an action other than rebuild or restart | Sync-type watch actions need a target path inside the container to sync into — only rebuild/restart actions can omit it. |
An external secret (secrets.mysecret.external: true) with neither file nor environment set | An external secret still needs to tell Compose where its value comes from locally — add file: or environment:. |
Close look-alikes that don't say "invalid compose project"
checkConsistency runs a few other checks that produce a plain, unwrapped error — genuinely similar in spirit, but without this specific suffix. Worth knowing so you're not searching for the wrong error text:
- A
build.additional_contextsentry (service:nameform) pointing at an unknown or non-buildable service. healthcheck.testnot starting withCMD,CMD-SHELL, orNONE.network_mode: "service:name"referencing a service that doesn't exist.- A negative
scaleordeploy.replicasvalue. - Two mounts claiming the same target path — a
tmpfsentry and avolumesentry both mounting/data, for instance.
checkConsistency also runs a dependency-cycle check (two services depends_on each other, directly or through a longer chain) as its very last step, via a separate graph-checking module — a real failure mode, just one that lives in a different part of the codebase from the sentinel-wrapped checks above.
The look-alike that isn't even the same check: an invalid project name
A completely separate validation path checks the project name itself — set via docker compose -p myname, COMPOSE_PROJECT_NAME, or a top-level name: field in the compose file — against the pattern ^[a-z0-9][a-z0-9_-]*$: lowercase letters, digits, underscores, and hyphens only, and it can't start with a hyphen or underscore. This fails with a differently-shaped message entirely — something like "MyCoolProject" is not a valid project name — not the service "X": invalid compose project shape covered above. The most common trigger is an uppercase letter, which recent Compose versions reject where older ones didn't. One sharp edge worth knowing: an invalid project name set via a .env file has been reported to fail silently in some Compose versions instead of showing any error at all — if commands are behaving strangely with no error output, check what your effective project name actually resolves to before assuming the compose file itself is fine.
How to debug it systematically
- Run
docker compose config. It loads and validates the file — including every check above — without starting anything, so you get the same error without the noise of adocker compose upfailure log. - Read only the text before the final colon. The suffix is always the same string; it tells you nothing about which of the ~19 checks above actually fired.
- Match the quoted service/network/volume/secret name against your file's top-level declarations, watching specifically for a typo, a rename that only got applied in one place, or a reference that predates a section getting deleted.
- If the message is genuinely unhelpful or seems to reference something that doesn't exist in your file at all, consider that you may have hit one of the rarer
extends-related edge cases in Compose itself rather than a mistake in your file — multi-levelextendschains have been reported to intermittently trip this exact error even on a config that's otherwise valid.
Try it yourself
Docker Compose Validator checks for the most common causes above — undeclared volumes/networks, undefined depends_on references, and a missing image/build — directly in your browser, before you ever run docker compose at all. Docker Compose Visualizer turns the file into a diagram of services, networks, and volumes, which makes a missing or renamed reference easy to spot visually. See Common Docker Compose Errors for full worked examples of the most common triggers, and Common Docker Compose Networking Errors for network-specific mistakes that are valid Compose but still break at runtime.
FAQ
Is "invalid compose project" itself telling me something is structurally wrong beyond one line?
No — despite the phrase "project" in it, it's not describing your whole project as broken in some deep way. It's a single generic error value reused for roughly nineteen unrelated single-point failures. One fixed reference or one resolved conflict is all it usually takes.
Can this happen with completely valid YAML?
Yes, and that's the whole reason it's confusing — every check that produces this error runs after YAML parsing has already succeeded. A file with perfect syntax can still fail here, because the problem is semantic (a dangling reference, a contradicted setting), not structural.
Why doesn't the error just say which check failed?
It effectively does — the text before the colon is that information, just not labeled with the internal check's name. The suffix itself is the part that adds nothing; a long-standing open issue against Compose tracks exactly this complaint.
I fixed the reference the error named, and it still fails. Now what?
checkConsistency returns on the first failure it finds, not all of them — so it's normal to fix one and immediately hit a second, unrelated one on the next run. Re-run docker compose config after every fix rather than assuming one pass caught everything.