Composing configurations
A Kranz project can be assembled from several configuration files. Every file is a complete, independently runnable configuration: a repository keeps its own kranz.yaml, and a workspace combines those files without copying them. Override layers change values on top of the result, and protected values enforce the ones that must not change.

Combine projects from the command line
Repeated -f composes several autonomous configurations into one project:
kranz -f repositories/catalog/kranz.yaml -f repositories/checkout/kranz.yaml
kranz -f 'repositories/*/kranz.yaml'Each file keeps its own project, ui, defaults, adjacent .env, and relative paths. Only its services and action groups join the project. If a file omits defaults.dir, Kranz treats it as . beside that file. Services and action groups without dir use that directory, and their actions inherit it unless an action sets its own dir. The parent config's directory does not replace this default for included files.
Include other configurations
A root file lists what it composes under include, by exact path, sorted glob, or bounded discovery:
project: Workspace
include:
- path: platform/kranz.yaml
- glob: services/*/kranz.yaml
- discover:
root: repositories
max_depth: 3Included files can include further files. A file reached twice loads once, a cycle is an error, and max_depth on an include entry limits how deep the include graph goes below it. When the working directory has no root file, Kranz discovers the configurations below it and composes them into a virtual project named after the directory. The configuration reference lists every rule.
Native kranz.yaml files are the recursive nodes of the tree. Procfile and supported process-compose.yaml files can appear anywhere as terminal leaves: their services join the same effective graph and keep paths relative to their own directories, but those formats cannot declare Kranz include or overrides sections. A composition-wide --override can still patch their effective services after the graph is assembled. Process Compose's conventional process-compose.override.yaml remains supported as part of that leaf.
Change values with override layers
An override layer is a partial file that patches services. Where the layer is declared decides what it can reach:
Inside a file. The file's own
overrides:list patches only that file's services, by their original names, before the file joins the project.yaml# repositories/catalog/kranz.yaml overrides: - overrides/local.yamlOn the command line.
--override PATH, repeatable and applied in order, patches the composed project, so it can reach any service by its display name.KRANZ_OVERRIDEsets the same list from the environment.bashkranz -f kranz.yaml --override kranz.local.yaml
# kranz.local.yaml — only the differences
services:
catalog/api:
ports: [18111]
env:
LOG_LEVEL: debugMappings merge, sequences replace, and null removes a value. A layer is a pure patch: it cannot declare defaults, include, overrides, or protected.
Enforce values with protected
protected values apply after composition and every override layer. When several files protect the same field, the outermost file wins:
protected:
services:
checkout/api:
env:
TLS_MODE: requiredService names
A service keeps its short name while it is unique. When two files declare the same name, both are qualified by the smallest useful directory prefix, for example catalog/api and checkout/api, and kranz config check reports a display_name_qualified diagnostic.
Dependencies are resolved after those display names have been allocated. The source that declares the reference is part of the lookup:
# repositories/checkout/kranz.yaml
services:
api:
command: ./serve
worker:
command: ./work
depends_on: [api]If another included file also declares api, the two services may be displayed as catalog/api and checkout/api. The worker dependency above still means the api from the checkout file. Kranz rewrites it to checkout/api in the effective graph; the author does not need to predict the name allocator's result.
If the declaring source has no local match, a bare reference may cross source boundaries only when the original name is globally unique. For example, a service in one file may use depends_on: [log-collector] when exactly one composed source declares log-collector. If two external sources declare that name, composition fails as ambiguous and lists their qualified display names. Kranz never chooses one based on include or discovery order.
An outer source can disambiguate an intentional cross-source dependency with the effective qualified name:
services:
root-task:
command: ./run-task
depends_on: [checkout/api]Qualified display names are derived from the current composition. Adding or removing a colliding source can change them, so prefer a same-source short name when the dependency belongs to that autonomous configuration. After every reload Kranz allocates names and resolves references again; an obsolete or newly ambiguous reference rejects the new configuration instead of silently retargeting the dependency.
The same rules apply to keys in dependency_conditions and to service references in before_start. Internally, each service also has a stable ID derived from its defining source and original key. That ID lets reload reconciliation recognize the same running service across a display-name change; display names remain the user-facing keys of the resolved dependency graph.
Check the result
kranz config checkvalidates the project and lists its sources and diagnostics, such assource_deduplicatedorinclude_depth_truncated.kranz config sourcesdraws every file as an include tree with the services each defined and the fields each overrode.--by-servicelists each service with its defining file and every later override. In the dashboard,mopens the same map.kranz config explain SERVICEshows the ordered chain for every field.
Try it
examples/composition in the repository is a runnable catalog of every rule on this page, including the cases that must fail. Its README lists a command and the expected result for each scenario.