1241 words
6 minutes

Docker Compose Include Relative Paths and env_file: How to Avoid CI Surprises

2026-08-10
2026-09-15
DevOps
Docker
/
DevOps
/
Containers
/
Troubleshooting

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:

  • include treats each Compose file as a self-contained module. Relative paths inside the included file resolve relative to that file’s project directory.
  • Multiple -f files 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.yaml
services/
db/
compose.yaml
db.env

The included file can own its relative references:

services/db/compose.yaml
services:
db:
image: postgres:18
env_file:
- ./db.env
volumes:
- ./data:/var/lib/postgresql/data

The root file includes the module:

compose.yaml
include:
- ./services/db/compose.yaml
services:
api:
build: ./api
depends_on:
- db

With 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/.env

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

Reproduce the working-directory boundary before changing paths#

The Compose include reference defines env_file as an interpolation input and project_directory as the base for relative paths. That is the contract to target, but the CLI version still matters. Docker Compose issue #11577 reports a narrower failure in v2.24.6: a top-level include env_file was looked up from the current working directory, so the root invocation worked while the same configuration from a subdirectory failed with Couldn't find env file. The issue is marked closed, but its public page does not document a fix or resolution detail; treat it as a bounded report, not proof that every current release behaves the same way.

Check the exact boundary used by CI before rewriting the repository layout:

Terminal window
docker compose version
# Run from the repository root.
docker compose config --environment
docker compose config --quiet
# Run from a subdirectory while naming the same root Compose file.
(
cd services/db
docker compose -f ../compose.yaml config --environment
docker compose -f ../compose.yaml config --quiet
)

Use the result to classify the failure:

ResultBoundary to inspectNext action
include is unknown or rejectedThe Compose CLI is older than the documented 2.20.0 minimumUpgrade and pin the Compose version used by CI.
Root and subdirectory commands render the same modelThe installed version matches the documented path behavior for this caseKeep the module-relative paths and pin the verified CLI version.
Root works but the subdirectory reports Couldn't find env fileThis matches the path-resolution failure reported for Compose v2.24.6Treat the working directory as an input, use a deterministic CI directory or explicit paths, then retest after upgrading.
config --environment changes between runsShell variables, --env-file, or project .env precedence changedCompare those inputs before changing a service-level env_file.

The --env-file CLI option has its own boundary: Docker documents its path as relative to the current working directory. Keep that path explicit in CI, and use docker compose config --environment followed by docker compose config to verify both interpolation inputs and the rendered model. These checks expose a path or interpolation problem before up creates containers.

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:

  1. A local command launched from the repository root.
  2. A CI command launched from a temporary checkout or a subdirectory.
  3. 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:

Terminal window
docker compose version
docker compose config

The 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:

Terminal window
docker compose -f compose.yaml -f compose.ci.yaml config

Do 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

Docker Docs: Merge Compose files

Docker Docs: Compose trust model

docker/compose issue #11577: include env_file path resolution

Docker Compose Include Relative Paths and env_file: How to Avoid CI Surprises
https://laplusda.com/en/posts/docker-compose-include-relative-paths/
Author
Zero
Published at
2026-08-10
License
CC BY-NC-SA 4.0