Docker Compose network declared as external, but not found
A docker compose network declared as external is one you promised already exists, and Compose takes that promise literally: it looks the network up by name when the stack starts and refuses to start anything when the lookup fails. Our recorded run shows the same compose file starting cleanly and then failing, with nothing changed between the two except whether the network was there.


What this error means
A docker compose up fails immediately, before any image is pulled and before any container is created, naming the network rather than a service. Nothing in the file is invalid: the same file works the moment the network exists, which is what makes this confusing on a runner, where it works on a developer machine that has had the network since last month. The recorded run below created the network, ran the stack to completion, removed the network, and ran the identical command again.
--- control: create the network, then up
Container compose-extnet-demo-web-1 Started
web-1 exited with code 0
--- remove the network, run the same command again
bridge host none
network shared-net declared as external, but could not be foundReproduced on a Latchkey runner
Docker version 29.7.2, build a7dcaa6
Docker Compose version v5.5.0
docker.io/library/alpine:3
--- control: create the network, then up
596b4d636dd567d504b505e28d5ee3d21c34be73ee2dfa2087a2b9571fe3a9ba
Container compose-extnet-demo-web-1 Starting
Container compose-extnet-demo-web-1 Started
web-1 exited with code 0
--- remove the network, run the same command again
shared-net
bridge host none
network shared-net declared as external, but could not be found
[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.
External means you own it
The Compose networks reference we read on 2026-09-20 puts it in one sentence: Compose does not attempt to create these networks, and returns an error if one does not exist. The same page notes that every attribute other than the name is irrelevant on an external network, and that a file setting any of them is rejected as invalid.
The reason to mark a network external is that another stack owns it. If nothing else uses it, the declaration costs you a setup step, a cleanup step and this failure, and buys nothing.
| Declaration | Who creates it | What the name is |
|---|---|---|
| A plain network entry | Compose, on up | The project name plus the key, for example api_default |
external: true | You, before up | Exactly the key, unless name: says otherwise |
external: true with name: | You, before up | Exactly the value of name: |
| No networks block at all | Compose, on up | The project default network |
Common causes
Nothing created the network in this job
The dominant cause on a runner. The file was written on a machine where the network already existed, and the job it runs in has no create step. Our recorded run reproduces exactly this: the same file, the same command, and the only difference is whether the network is there.
The name does not match the network that exists
Compose prefixes the networks it creates with the project name, so a network another project created is rarely called what your key calls it. The declaration names a network that does not exist while a nearly identical one does, which reads as a Compose bug and is a naming mismatch.
A prune removed it between steps
Cleanup steps are cheerfully destructive: an unused network has no running containers attached and is exactly what a system prune collects. In our experience this is the cause whenever the same job creates the network successfully and fails on it later.
The network belongs to another runner or another job
A network created in one job does not exist in the next, because the next job starts on a different machine. The same applies to a self-hosted runner that was recycled between jobs, and to any step that expects yesterday state on a fresh runner.
How to fix it
Create the network in the job, idempotently
- Inspect first, create only if the inspect fails, so a second run is harmless.
- Put it before the step that starts the stack, in the same job.
- Remove it in a cleanup step only if the job created it.
docker network inspect shared-net >/dev/null 2>&1 \
|| docker network create shared-net
docker compose up -d --waitLet Compose own the network instead
If no other stack attaches to it, drop the external declaration and let Compose create and remove the network with the project. That deletes the failure mode rather than working around it, and it removes a cleanup step you were probably not running.
# before
networks:
shared-net:
external: true
# after
networks:
shared-net: {}Name the real network when it is genuinely shared
When another project owns the network, write its actual name with the name attribute. Take the name from the network list rather than from the other project's compose file, because the prefix comes from the project name in force when it was created.
docker network ls --format '{{.Name}}'
# then, in compose.yaml:
# networks:
# shared-net:
# external: true
# name: tools_shared-netKeep prune away from the stack
Scope cleanup to what the job created, and run it at the end rather than between steps. A targeted compose down removes the project's own containers and networks and leaves shared ones alone, which a system prune does not.
docker compose down --remove-orphans # not: docker system prune -fWhy it is a CI failure and not a laptop failure
A developer machine accumulates networks. Somebody ran the create command once, months ago, and every stack since has found it waiting. A runner starts with the default networks and nothing else, which our recorded run shows in the line before the failure: bridge, host and none, and no project network of any kind.
That makes this a pipeline ordering question rather than a compose question. Whatever creates the network has to run in the same job, before the stack starts, and it has to be idempotent because the job may run twice on the same runner in a self-hosted setup.
- run: docker network inspect shared-net >/dev/null 2>&1 || docker network create shared-net
- run: docker compose up -d --waitWhen the network exists and the lookup still fails
Then the names do not match, and the usual reason is that Compose prefixes the networks it creates with the project name. A network created earlier by another Compose project is called something like tools_shared-net, and a file declaring shared-net as external will not find it. The fix is to say the real name with the name attribute rather than renaming anything.
The other reason is timing rather than spelling: a cleanup step running docker system prune between two steps of the same job removes unused networks, and a network with no running containers attached qualifies. If your log shows a create, a success and then a failure later in the same job, look for the prune.
networks:
shared-net:
external: true
name: tools_shared-netWhat the runner does about it
No repair, and the recorded run shows the round trip: the wrapper posted the failure to the sidecar, the sidecar answered, and the job ended on the same lookup. Latchkey has no pattern for this and should not have one. Creating a network that a compose file declared external would be a runner inventing the thing whose absence is the error, and on a shared network that is somebody else's resource.
How to prevent it
- Create shared networks in the job that needs them, before the stack starts.
- Use
external: trueonly where another stack genuinely owns the network. - Write the real network name with
name:rather than relying on a prefix. - Keep prune steps at the end of a job and scoped to what the job created.
Frequently asked questions
Does docker compose create external networks?
What does external true mean in docker compose?
How do I create a docker network before compose up in CI?
Why does my compose network have a project name prefix?
shared-net in a project called tools becomes tools_shared-net. An external declaration does no prefixing, which is why a name that another project created has to be written out in full with the name attribute.Related guides
References
- Docker docs: Compose networks top-level element, external networks
- Docker docs: the Compose file reference
- elevennines-inc/swagger-all-in-one-docker-compose#5: the error in a stack that expected a shared network
- research-software-directory/RSD-production#2: the same failure while following a README on a local machine
- Docker documentation
- Docker build cache
- GitHub Actions documentation