August 3, 2023·4 min read·engineeringdockercomposeconfiguration

Compose include preserves the paths of reusable application definitions

Desktop 4.22 adds Compose project includes. Loading each included definition in its own directory solves a different problem from merging override files.

A reusable Compose file needs its relative paths to keep their meaning when another application consumes it. Combining files as overrides does not necessarily provide that behavior. Docker Desktop 4.22, released August 3, adds support for Compose include. The new attribute lets a larger application import a component's definition while preserving the directory context in which its author wrote it.

The practical problem is ownership. A component may have a Dockerfile and supporting files beside its Compose definition. Another component needs to run it without relocating those files or rewriting its paths. Splitting YAML into smaller documents is useful only if the documents can continue to describe their own inputs correctly.

I value Docker configuration when it makes an environment repeatable between machines. Relative paths are part of that description, so a reuse mechanism should preserve their interpretation. An include that imports a component's working definition is more useful than a file split that leaves every consumer responsible for reconstructing its directory assumptions.

An include has a directory context

Nicolas De Loof's August 1 explanation distinguishes inclusion from earlier reuse mechanisms. extends selects a service whose configuration the consumer understands. Combining files with -f merges an application model and supports overrides. include imports a whole definition while preserving the original location used to interpret its relative references.

Those are different operations. An override is useful when a project wants to modify a service it already knows. An included component can contain supporting resources the consumer should not need to reconstruct. The choice follows from whether the consumer is editing a definition or consuming a component maintained elsewhere.

The Compose loader implementation makes the directory behavior explicit. When no project directory is supplied, the loader derives it from the first included path. It then loads the imported configuration with that working directory and path resolution enabled. The directory context is part of the load operation, not an informal convention imposed on the user.

Consider an illustrative application with a component definition at ../database/compose.yaml. The root definition can include it and refer to its service by name. The included file must actually define the database service for this example to form a valid combined application.

include:
  - ../database/compose.yaml

services:
  webapp:
    image: example/webapp:1.0
    depends_on:
      - database

The image reference is a placeholder for the application's own image. The example's relevant behavior is the include and service dependency. Paths inside the database definition are interpreted using that definition's project directory. The web application does not need to copy the database's build context into its own folder.

Imported resources share one application model

The loader imports services as well as networks and volumes. It also imports secrets and configuration objects. That is why copying a single service definition is an incomplete substitute for importing a component. A service may refer to resources that belong to the same maintained definition.

The import operation also checks for conflicting resource names. A conflicting service produces an error, and the implementation applies corresponding checks to the other resource categories. Inclusion therefore does not create arbitrary independent namespaces inside one application. Component authors still need resource names that can coexist in the combined model.

That restriction follows from what the consumer is asking Compose to construct. The result is one application model containing the imported resources. Two definitions that both claim the same service name would leave the intended resource ambiguous. An error makes that collision visible instead of silently choosing one component's definition.

This has a direct consequence for a component's maintenance contract. Names used by other services become part of how the component is consumed. Internal file layout can change while the published service name remains stable. Renaming that service changes references in consumers, even if all of the component's containers still work when run alone.

Reuse changes where integration work happens

The new mechanism lets a component author maintain a complete local definition. Its consumer declares the dependency in configuration, so the invocation does not need to carry a growing list of additional files. A normal Compose command can read the declared includes with the rest of the application description.

This improves the visibility of configuration dependencies. A reviewer reading the root file can see which other definitions it imports. An invocation assembled elsewhere in a script can be harder to discover from the YAML alone. Keeping the dependency beside the services makes the construction of the application model easier to explain.

The included definition still needs a clear purpose. A consumer that needs to override many internal details may be asking for a different interface from one that merely starts a supporting component. Inclusion provides a loading mechanism and path semantics. It does not determine which responsibilities belong to each part of an application.

I prefer the explicit dependency because it allows the configuration to describe its own composition. A developer opening the root file can follow the include to the component that owns the relevant settings. That is a concrete improvement in maintainability when several independently maintained parts need to run together.

Resource Saver addresses a different cost

Desktop 4.22 also introduces experimental Resource Saver on Mac and Windows Hyper-V. The release notes describe reduced resource usage when no containers are running. This concerns the cost of leaving the local environment idle, while Compose inclusion concerns how the environment is described.

The backend qualification matters. On Windows, the entry names Hyper-V, so the release should not be summarized as identical behavior across every Windows backend. The experimental setting also has a different status from the Resource Usage view, which the notes separately promote to general availability. These labels identify distinct capabilities within the same product update.

For Compose users, the immediate configuration change is the ability to load a maintained component with its own path context. Resource names still need to coexist, and dependencies on imported services remain explicit. The combination allows a root application to refer to another definition without rewriting that definition's relative build and file references.