Labels
How a compose file describes itself: the addresses it answers on, the ports it must keep, and the values it needs from its universe.
Why labels
A compose file already knows which port its service listens on and which settings depend on the environment. Writing that into a manifest means saying it twice and keeping the two in step by hand, so multiversectl reads the compose files instead — with docker compose config, so it sees the merged result, variables and profiles included.
Labels rather than a reference such as ${MULTIVERSE_ORIGINS} in the service's own environment:, because a compose file is also run by hand: the reference resolves to the empty string when multiversectl is not driving it, while a label is inert metadata until it is.
Addresses: multiverse.host.*
The label key names the host, the value is the container port:
services: app-dev: labels: multiverse.host.app: "4200" # app-u3.<domain> → 4200 multiverse.host.api: "8001" multiverse.host.api.entrypoint: devapi # default: web multiverse.host.api.aliases: app,adm # same service, more names
aliases is how one API answers under each front-end's own hostname on a second entrypoint, which is what makes a hard-coded :8001 work in every universe.
With no labels at all
A service exposing exactly one port is reached at the app's own name, so the common case needs nothing. Anything ambiguous is an error that names the label to add rather than a guess:
api: services "web" and "worker" both expose a port — label the one that should answer: multiverse.host.api: "<port>"
Hosts declared in the manifest's hosts: win over labels, and exist for repositories you cannot change.
Ports: multiverse.publish
services: db: labels: multiverse.publish: "true"
Keeps this service's published host ports instead of dropping them. Only one universe can hold a given host port at a time, so a service that asks for this is a service only one universe can run — usually worth it for a database you point a GUI at, rarely worth it for anything else. The manifest's publish: list does the same from outside.
Values: multiverse.env.*
The label key names an environment variable, the value names what to put in it:
services: web: labels: multiverse.env.DJANGO_ALLOWED_HOSTS: hosts multiverse.env.DJANGO_CSRF_TRUSTED_ORIGINS: origins multiverse.env.DJANGO_CORS_ALLOWED_ORIGINS: origins multiverse.env.API_BASE_URL: url:devapi
| Source | Value |
|---|---|
origins | Every address of the universe as an origin, each with its entrypoint's port: http://api-u3.d,http://devapi-u3.d:8001,… |
hosts | Every hostname, comma separated. |
domain | The wildcard domain in use. |
universe | The universe's name, as it was typed. |
slug | The part its hostnames carry. |
host:<key> | One host key's hostname. |
url:<key> | One host key's whole address, port included. |
An unknown source fails the start, naming the valid ones. That is deliberate: a variable left quietly empty surfaces much later as a confusing runtime error, a long way from the label that caused it.
It beats env_file
These land in the generated fragment, which is merged last, so they override both the service's own environment: and any env_file:. A static list of origins in a committed .env therefore stops being what the universe runs with — which is the usual reason CORS is wrong in a second copy of a stack.
Prefer origins and hosts over naming addresses one at a time: they follow the manifest, so adding a host key or an app does not leave a settings list behind to be found later.
Where multiversectl reads them
Labels are read from the merged configuration of the app's files, at two moments: when a universe is created, so urls and ls can answer without a running container, and again on every start, because the files may have changed since. Nothing is cached beyond the universe's state file.
This also means a label added to a repository takes effect on the next up, with no manifest change and no re-creation.