multiversectl GitHub

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 isPut it
True of the project everywhereThe repository's own compose file. It is not multiversectl's business.
True of this environment, for a repository you controlA label on the service. The repository then carries its own needs — see Labels.
True of this environment, for a repository you do not controlThe app's inline overlay: in the manifest.
Several services' worth, or longA 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.yml in the repository's checkout.
  • overlays/<app>.yml beside 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 — !reset is 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:

FieldMeaning
profilesCompose profiles always active for this app, so a dev-profiled service starts and a prod-profiled one does not.
servicesThe 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.
optionalNamed 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 workers

ls -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".