mocker version: 0.7.2 (Homebrew) — macOS, Darwin 25.5.0, arm64, Apple container backend
Summary
The Compose Spec top-level include: element (Docker Compose v2.20+) is not implemented. Rather than erroring on an unsupported key, mocker parses the file, drops every included model, and reports success: compose config emits an empty services: map and compose up <service> exits 0 having created nothing.
This makes mocker unusable with any project that splits services across files. Concrete case: Laradock v20.x, whose root docker-compose.yml has no services: key at all — ~120 services arrive entirely through include:. mocker compose up -d caddy there creates the 2 networks and 17 volumes declared in the root file, then exits 0 with no container, no warning, and no non-zero status.
Reproduction
/tmp/mocker-inc/
docker-compose.yml
svc/compose.yml
docker-compose.yml:
include:
- path: svc/compose.yml
project_directory: .
svc/compose.yml:
services:
hello:
image: docker.io/library/alpine:3.20
command: ["sleep","300"]
Actual:
$ mocker compose config
name: default
services:
$ mocker compose config --services
# empty
$ mocker compose up -d hello
[+] Running 0/0
$ echo $?
0
Control — identical services: block written inline (no include:) works correctly, container and volume are created. So the runtime path is fine; only the reader is missing.
Expected (Compose Spec include)
- Each entry is short form (
- path/to/compose.yml) or long form {path, project_directory, env_file}; path may be a single string or a list.
- The included file is parsed as its own model. Relative paths inside it resolve against
project_directory, defaulting to the included file's own directory.
- Interpolation of the included file uses that entry's
env_file, defaulting to .env under its project_directory.
- The resulting model is merged into the including model, recursively, before
-f overlay merging.
- Two includes defining the same service with conflicting content is an error, not a silent override. The parent's own inline definitions win over included ones.
So for the repro: config --services prints hello, and up -d hello starts one container.
Notes on implementation
Reading Sources/MockerKit/Compose/ComposeFile.swift at main, the primitives this needs already exist:
ComposeFile.merge(_:) (L220) — ordered multi-file merge with per-service merged(with:) field merging and networks/volumes union.
load(from:projectDir:) (L53) → parseAndSubstitute(content:projectDir:) (L72), which resolves .env from projectDir and interpolates before YAML parsing — the correct order for per-file include env scoping.
Sketch:
- Decode a top-level
include: key in parse into [ComposeInclude] (short + long form, path as string or list).
- Have
parseAndSubstitute accept an explicit env dictionary instead of hardcoding projectDir/.env (L73), so an entry's env_file can override it.
- After parsing the parent, resolve each entry's
path relative to the including file's directory, default project_directory to the included file's directory, and recurse through load.
merge(includedModels + [parentInlineModel]) so parent wins; raise an error on conflicting duplicate service keys between two includes.
- Cycle guard: visited set of canonicalized absolute paths, plus a depth cap.
Point 3 intersects the already-fixed #49 / #60 (relative bind-mount resolution) — Laradock uses project_directory: . precisely so that included services' relative mounts resolve against the repo root, so those fixes must apply using the include's project directory, not the top-level one.
Suggested tests in Tests/MockerKitTests/ComposeFileTests.swift: short-form include; long-form project_directory affecting a relative bind mount; long-form env_file affecting interpolation; nested include; cycle detection; conflicting-service error; parent-overrides-include precedence.
Minimum acceptable interim behavior
If the full element is out of scope right now, please fail loudly — exit non-zero with something like unsupported Compose element: include instead of silently yielding an empty project. Silent success is worse than an error here: it looks like a working stack.
Workaround in use
Flattening with upstream Compose and feeding the result to mocker works, and mocker parses all 124 resulting services with none dropped:
docker compose config > /tmp/flat.yml
mocker compose -f /tmp/flat.yml -p <project> --project-directory <repo> up -d --build <service>
Related: the silent-success half of this was much harder to diagnose because of the unknown-service behavior filed separately.
mocker version: 0.7.2 (Homebrew) — macOS, Darwin 25.5.0, arm64, Apple
containerbackendSummary
The Compose Spec top-level
include:element (Docker Compose v2.20+) is not implemented. Rather than erroring on an unsupported key, mocker parses the file, drops every included model, and reports success:compose configemits an emptyservices:map andcompose up <service>exits 0 having created nothing.This makes mocker unusable with any project that splits services across files. Concrete case: Laradock v20.x, whose root
docker-compose.ymlhas noservices:key at all — ~120 services arrive entirely throughinclude:.mocker compose up -d caddythere creates the 2 networks and 17 volumes declared in the root file, then exits 0 with no container, no warning, and no non-zero status.Reproduction
docker-compose.yml:svc/compose.yml:Actual:
Control — identical
services:block written inline (noinclude:) works correctly, container and volume are created. So the runtime path is fine; only the reader is missing.Expected (Compose Spec
include)- path/to/compose.yml) or long form{path, project_directory, env_file};pathmay be a single string or a list.project_directory, defaulting to the included file's own directory.env_file, defaulting to.envunder itsproject_directory.-foverlay merging.So for the repro:
config --servicesprintshello, andup -d hellostarts one container.Notes on implementation
Reading
Sources/MockerKit/Compose/ComposeFile.swiftatmain, the primitives this needs already exist:ComposeFile.merge(_:)(L220) — ordered multi-file merge with per-servicemerged(with:)field merging and networks/volumes union.load(from:projectDir:)(L53) →parseAndSubstitute(content:projectDir:)(L72), which resolves.envfromprojectDirand interpolates before YAML parsing — the correct order for per-file include env scoping.Sketch:
include:key inparseinto[ComposeInclude](short + long form,pathas string or list).parseAndSubstituteaccept an explicit env dictionary instead of hardcodingprojectDir/.env(L73), so an entry'senv_filecan override it.pathrelative to the including file's directory, defaultproject_directoryto the included file's directory, and recurse throughload.merge(includedModels + [parentInlineModel])so parent wins; raise an error on conflicting duplicate service keys between two includes.Point 3 intersects the already-fixed #49 / #60 (relative bind-mount resolution) — Laradock uses
project_directory: .precisely so that included services' relative mounts resolve against the repo root, so those fixes must apply using the include's project directory, not the top-level one.Suggested tests in
Tests/MockerKitTests/ComposeFileTests.swift: short-form include; long-formproject_directoryaffecting a relative bind mount; long-formenv_fileaffecting interpolation; nested include; cycle detection; conflicting-service error; parent-overrides-include precedence.Minimum acceptable interim behavior
If the full element is out of scope right now, please fail loudly — exit non-zero with something like
unsupported Compose element: includeinstead of silently yielding an empty project. Silent success is worse than an error here: it looks like a working stack.Workaround in use
Flattening with upstream Compose and feeding the result to mocker works, and mocker parses all 124 resulting services with none dropped:
Related: the silent-success half of this was much harder to diagnose because of the unknown-service behavior filed separately.