Docker Compose "service refers to undefined volume" in CI
A service mounts a named volume that Compose does not know about. Named volumes must be declared in the top-level volumes: section, and this one is missing.
What this error means
A docker compose up/config fails during validation with service "X" refers to undefined volume "Y": invalid compose project. Nothing starts because the volume reference cannot be resolved.
service "db" refers to undefined volume pgdata: invalid compose project
# the service mounts "pgdata:/var/lib/postgresql/data" but there is no
# top-level volumes: pgdata:Diagnose it: read the resolved config, not the file you wrote
Compose merges override files, interpolates variables, and applies defaults before it does anything. Most Compose failures in CI are visible in the resolved configuration and invisible in the source file, because the value you are debugging came from an unset variable that quietly became an empty string.
# the fully merged, interpolated configuration Compose will actually run
docker compose config
# fail loudly on unset variables instead of silently interpolating empty
docker compose --env-file .env config --quiet || echo "invalid"
# which override files were picked up
docker compose config --services
Common causes
The named volume is not declared at the top level
A service uses pgdata:/path, but there is no pgdata: under the top-level volumes: key. Compose treats the bare name as a named volume that must be declared.
A typo between the mount and the declaration
The volume is declared under one name and referenced under a slightly different one, so the reference resolves to nothing.
How to fix it
Declare the named volume
Add the volume to the top-level volumes: section.
services:
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:Or use a bind mount instead
If you meant a host path, use a relative/absolute path (starting with ./ or /) rather than a bare name.
volumes:
- ./data:/var/lib/postgresql/data # bind mount, no top-level declaration neededBind mounts behave differently on a runner
- A relative bind source is resolved against the compose file location, not the working directory of the shell that invoked it.
- The host path must exist before
up. Compose creates missing directories for named volumes but not for bind mounts, and the failure surfaces as a mount error rather than a missing-path error. - On a CI runner the workspace path differs from your machine, so any absolute host path in a compose file is a portability bug waiting for its first CI run.
- Prefer named volumes for anything that does not genuinely need to be read from the host. They remove the whole class of problem.
How to prevent it
- Declare every named volume in the top-level
volumes:section. - Run
docker compose configin CI to catch undefined volumes early. - Keep volume names consistent between the mount and the declaration.