Overlays and merge order
The files that make up an app, the order they are merged in, and where to put a value that only this environment knows.
Merge order
An app is a list of compose files, merged left to right by the container runtime, with the last word winning. multiversectl adds two of its own at the end:
1. docker-compose.yml # the repository's own, unchanged 2. overlays/<app>.yml # beside the manifest, if present 3. .multiverse-overlay-<app>.yml # the manifest's inline overlay: 4. .multiverse-routes-<app>.yml # generated: routing, ports, labels
Everything before the generated files is yours. Nothing rewrites your compose file, and removing multiversectl leaves a repository that still runs with plain docker compose up.
Within compose's merge rules the shapes matter: mappings merge key by key, so an environment: entry in a later file replaces just that variable; sequences are replaced wholesale, so a later command: or ports: supersedes the earlier one entirely.
Which file to put something in
| The value is | Put it |
|---|---|
| True of the project everywhere | The repository's own compose file. It is not multiversectl's business. |
| True of this environment, for a repository you control | A label on the service. The repository then carries its own needs — see Labels. |
| True of this environment, for a repository you do not control | The app's inline overlay: in the manifest. |
| Several services' worth, or long | A file beside the manifest, named in compose: with an @ prefix. |
Prefer the label. It keeps the knowledge in the repository that has it, and it is inert when the compose file is run by hand.
Found by convention
With no compose: list, two conventional places are tried, and whichever exist are used in this order:
docker-compose.ymlin the repository's checkout.overlays/<app>.ymlbeside the manifest — or<manifest-name>/<app>.yml, for a directory holding several manifests.
So an app that follows the convention needs no compose: line at all, and a repository that no apps: entry names gets an app of its own name on the same terms.
When you do name the files, a plain path is relative to the repository's checkout and a path starting with @ is relative to the manifest's directory:
apps: - name: api repo: api compose: [docker-compose.yml, docker-compose.dev.yml, "@acme/api.yml"]
The inline overlay
overlay: is a compose fragment written straight into the manifest, kept verbatim and merged third. Use it for the few things only this environment knows:
apps: - name: web repo: web profiles: [dev] overlay: services: app-dev: # The dev server rejects Host headers it does not know, # and this universe's hostname is not in its config. command: sh -c "npm start -- --allowed-hosts=${APP_HOST}"
It is compose syntax, so it can reference any of the variables a universe passes. It is also the whole service definition for a repository that has no compose file of its own:
apps: - name: docs repo: docs overlay: services: site: image: nginx:alpine volumes: ["${DOCS_DIR}:/usr/share/nginx/html:ro"] labels: multiverse.host.docs: "80"
The generated fragment
The last file is written by multiversectl before each start, and it is where the environment-specific mechanics live:
- Proxy labels for every host rule, and the proxy network added to the services that serve one.
ports: !reset []on every service that is not allowed to publish —!resetis compose's way to clear a sequence an earlier file set, rather than add to it.- An
environment:block for the values a service asked for by label.
Because it is merged last, its environment: beats both the service's own environment: and any env_file:. That is what lets a per-universe value override a static list in a committed .env — the one thing an overlay in the manifest cannot do.
Profiles and service selection
Three different ways to say which services run, in increasing specificity:
| Field | Meaning |
|---|---|
profiles | Compose profiles always active for this app, so a dev-profiled service starts and a prod-profiled one does not. |
services | The services to start. Omit it and every service the files define for the active profiles starts, which is what docker compose up would do. Name them only when a file also holds services a universe should not run. |
optional | Named groups started on demand with --with <name>. The name is activated as a profile too, so a service can be in a group by either route. |
apps: - name: api repo: api services: [web, db] optional: - name: workers services: [worker, scheduler]
$ multiversectl up 3 api --with workersls -o wide shows every service the files define with its state, including the ones that are off and the flag that would start them, so "what is not running" is as answerable as "what is".