Skip to content

mocker compose silently ignores top-level include: — all included services dropped, exit 0 #69

Description

@KennethWKZ

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)

  1. 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.
  2. 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.
  3. Interpolation of the included file uses that entry's env_file, defaulting to .env under its project_directory.
  4. The resulting model is merged into the including model, recursively, before -f overlay merging.
  5. 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:

  1. Decode a top-level include: key in parse into [ComposeInclude] (short + long form, path as string or list).
  2. Have parseAndSubstitute accept an explicit env dictionary instead of hardcoding projectDir/.env (L73), so an entry's env_file can override it.
  3. 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.
  4. merge(includedModels + [parentInlineModel]) so parent wins; raise an error on conflicting duplicate service keys between two includes.
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions