Docker Compose Include Relative Paths and env_file: How to Avoid CI Surprises
If docker compose include works from one directory but fails in CI, check the path owner before changing the shell command. Compose resolves a file included with include relative to that included file’s project directory, while ${PWD} comes from the directory where you invoked Compose. Use project_directory and env_file deliberately, then inspect the merged model with docker compose config.
Why include behaves differently from -f
Docker Compose supports two different ways to combine files:
includetreats each Compose file as a self-contained module. Relative paths inside the included file resolve relative to that file’s project directory.- Multiple
-ffiles are merged into one model. Relative paths in the merged files resolve relative to the first, base Compose file.
That distinction matters when a service refers to a build context, a bind mount, an env_file, or another nested Compose file. A layout that is correct for -f can break when the same file becomes an included module.
Give an included module its own path context
Suppose the repository is organized like this:
compose.yamlservices/ db/ compose.yaml db.envThe included file can own its relative references:
services: db: image: postgres:18 env_file: - ./db.env volumes: - ./data:/var/lib/postgresql/dataThe root file includes the module:
include: - ./services/db/compose.yaml
services: api: build: ./api depends_on: - dbWith include, ./db.env, ./data, and any build or nested-include paths in the module are resolved from services/db, not from whichever directory happened to launch the root command. This is the main reason to use include for modules that should remain portable.
Use long syntax when the parent must control the context
The long syntax lets the parent set a project directory and an interpolation file for an included Compose file:
include: - path: ./services/db/compose.yaml project_directory: ./services/db env_file: - ./services/db/.envUse project_directory when the module’s relative build, mount, or nested-include paths need an explicit base. Use env_file when the module expects interpolation values that should be supplied by the parent. Keep the path forms consistent with your repository layout, and let docker compose config tell you which file and value Compose actually resolved.
Do not assume that the include-level env_file changes the shell environment. It supplies default values for interpolation in the included Compose model; it does not export variables to the shell that launched the command. A service-level env_file is a separate Compose field that populates a service’s environment. For values that must exist before Compose parses the file, use a deliberate shell environment or a project .env file and document that boundary.
Avoid the ${PWD} trap
${PWD} is expanded from the invoking shell, not from the directory containing the included Compose file. It can therefore produce three different results:
- A local command launched from the repository root.
- A CI command launched from a temporary checkout or a subdirectory.
- A nested Compose invocation launched by a script with its own working directory.
Prefer paths relative to the Compose module when the resource belongs to that module. If an absolute host path is genuinely required, make the input explicit in CI and validate it before starting services rather than hiding the dependency in ${PWD}.
Check the Compose version first
The include feature requires a recent Compose CLI. Docker documents it for Compose 2.20.0 and later, and the current multiple-file guide uses 2.20.3 or later for the documented workflow. A runner with an older plugin can report that include is an unknown or unsupported property even when the YAML is otherwise valid.
Pin or preflight the Compose version in the same environment that builds the application:
docker compose versiondocker compose configThe second command should be run from the intended project directory with the same environment and --env-file choices used by deployment. It expands the model and surfaces missing files, invalid service references, and interpolation mistakes before containers start.
Choose include or multiple -f files
Use include when each file is a module with its own paths, environment defaults, and nested includes. Use multiple -f files when you intentionally want an override or merge model anchored to one base file:
docker compose -f compose.yaml -f compose.ci.yaml configDo not switch between the two only to silence a path error. First decide who owns the path. If a CI override should replace a service property, -f is usually the clearer contract. If a database or observability stack should be reusable from its own directory, include makes that ownership explicit.
Treat remote includes as code
Compose can load remote include sources, including Git and OCI-backed sources. Docker’s trust-model documentation warns that an included file can read referenced files and load nested includes. Review remote Compose content before allowing it into a production or CI project, and use docker compose config to inspect the resulting model.
For a related runtime workflow, Docker Compose Watch sync and rebuild behavior covers a different question: how an already-correct Compose model reacts to source changes.
FAQ
Q: Are paths in an included Compose file relative to the root file?
A: Not by default. include resolves relative paths from the included file’s project directory. Set project_directory when the parent needs to define that context explicitly.
Q: Why does ${PWD} point to the wrong folder in CI?
A: ${PWD} is supplied by the invoking shell. It describes the command’s working directory, not the location of the included Compose file. Use module-relative paths or make the CI path an explicit input.
Q: Should I use include or -f for a CI override?
A: Use -f when you want to merge an override against a base file. Use include when the files are independent modules that should retain their own path context.
References:
Docker Docs: Compose include reference
Docker Docs: Include Compose files
Report a typo or broken link, or suggest a related topic.