Skip to content

Parameters

A parameter is a value the release declares and the operator chooses — a port, a log level, an upload limit. The release says which exist and what each accepts; the manager refuses anything else.

Declared, rather than free-form, because a free-form setting has no failure mode for a typo: htpp_port: 9000 would be accepted, ignored, and discovered much later as "the port did not change".

Setting one

After install, with config set. At install time:

morzer init --release ./bundle --set http_port=9000 --set log_level=debug

--set needs a --release to validate against — there is nothing to check a value against without the manifest that declares it — and a value the release does not accept is refused before anything is created:

[4/6] stage release bundle
        failed: parameter "log_level"
        hint: log_level accepts debug, info, warn, error; default info -- Application log verbosity

The chosen values are recorded in the installation's state, and shown in installation.yaml:

parameters:
  http_port: "9000"
  log_level: debug

Stored as written, and re-validated against the release on every operation. A release that later narrows an enum, or drops a parameter, surfaces as an error naming the value rather than silently reinterpreting it.

Where a value goes

Every declared parameter reaches three places, always under the same name, and always present — an unset one carries the release's default, so nothing has to tell "unset" from "set to the default":

Consumer Form Example
Compose files <PRODUCT>_PARAM_<NAME> ${DEMO_PARAM_HTTP_PORT}
Configuration templates .Parameters.<name> {{ .Parameters.http_port }}
Hooks <PRODUCT>_PARAM_<NAME> $DEMO_PARAM_HTTP_PORT

The PARAM_ in the middle is not decoration. Without it a parameter named data_dir would export DEMO_DATA_DIR — a variable the manager already owns — and take the deployment's storage with it.

The manifest follows the value

Two manifest fields interpolate parameters, and they are the two that would otherwise contradict a changed port:

requirements:
  ports: ["{{ .Parameters.http_port }}"]

health:
  checks:
    - name: api
      type: http
      url: "http://127.0.0.1:{{ .Parameters.http_port }}/health/ready"

Without this, changing a port gives you a deployment that works and an apply that fails: Compose publishes 9000, preflight checks that 18080 is free, and the health probe asks 18080 and times out. The three have to move together.

Only these two fields, and the only thing they can see is .Parameters — not paths, not secrets, not the installation. A health URL able to interpolate a secret path is a health URL able to put one in a log line.

Only declared values reach the runtime

Setting DEMO_PARAM_HTTP_PORT in your shell does nothing. The manager builds the runtime's environment from an allow-list plus the declared parameters, so the recorded configuration is what runs.

This is deliberate, and it closed a real hole: the environment used to be inherited wholesale, which meant any <PRODUCT>_* variable in a shell interpolated into a Compose file — undocumented, unvalidated, unrecorded, and invisible to the manifest that was supposed to describe the release.

What still passes through is what a tool needs to run at all: PATH, HOME, TMPDIR, the XDG_* directories, Docker's own client configuration (DOCKER_HOST, DOCKER_CONTEXT, DOCKER_CONFIG, the TLS variables), SSH_AUTH_SOCK, and the proxy variables.

Types

Type Accepts Normalised to
port 1–65535 the number
int a whole number the number
bool true, false, 1, 0, TRUE… true or false
string anything trimmed
enum one of the declared values as written
duration 30s, 5m, 2h canonical form
bytes 512KiB, 25MiB, 2GiB canonical form

Values are normalised, so 9000 and 9000 cannot become two different ports downstream. Port 0 is refused: Compose reads it as "pick any", and the health check would then probe a port nothing is listening on.

Parameters are not secrets

A parameter's value is visible in docker inspect, in status --json, in the journal, and in installation.yaml in the clear.

Anything that must not be visible is a secret. Secrets are declared in their own schema, encrypted at rest, rendered to a tmpfs directory as files with mode 0400, never passed as environment, and audited by doctor. See Secret commands and How secrets work.

Changing one after install

See Changing configuration for the operator's walkthrough. In short:

morzer config set takes the deployment lock, validates the value, records it, re-renders the configuration and re-creates the services the release says depend on it.

morzer config list                    # every parameter, its value, and where it came from
morzer config get http_port           # the value alone, for a script
morzer config set http_port=9000      # validate, record, re-create the dependants
morzer config unset http_port         # back to the release's default
morzer config set http_port=9000 --dry-run   # the step list, and the config diff

morzer config get prints the value alone, with no decoration, so it substitutes directly into a script. config list names the source of every value, so what have I changed on this machine is answerable without diffing against the bundle:

NAME                 TYPE       VALUE            SOURCE
http_port            port       9000             installation
log_level            enum       info             release

Installation settings are not parameters

A parameter is declared by the vendor and reaches the deployment. An installation setting is your arrangement with the manager — whether it may contact a registry, which reference it follows — and changes nothing that is running.

They share morzer config because that is where you look to change a value, and they cannot collide: a parameter name is lowercase letters, digits and underscores, so a dotted name is always a setting.

morzer config settings                       # every setting, its value and what it means
morzer config set update.check=true          # a setting: a flag, no services touched
morzer config unset update.channel           # back to absent, which is always the safe state
Setting Meaning
update.check Contact the vendor's registry unprompted, for doctor and status. Absent means off.
update.channel A mutable reference to follow. See following a channel.
update.auto_apply Install what the channel offers, when the release declares a failure cannot end needing a database restore.
update.schedule When the update timer follows the channel — and installs, if update.auto_apply allows — as a systemd OnCalendar expression. This is the maintenance window. Absent takes daily at 03:30. See unattended updates.
update.randomized_delay How long each update tick may wait at random, as a duration in whole seconds such as 30m; 0 for none. Absent takes 30 minutes, or none on a dev-mode machine. morzer status shows it with the schedule.
backup.schedule When scheduled backups run, as a systemd OnCalendar expression. Absent takes the nightly default.
backup.scheduled Whether this manager runs backups on a timer at all. Absent means it does.
backup.before_update Whether an update takes a backup before it changes anything. Absent means it does. Only a dev-mode machine may set false; on a production machine, update --skip-backup --force skips it for one update.
retention.pre_update_backups How many of the newest pre-update backups retention keeps, on top of the ordinary count; at least 1. Absent takes 3. See retention.

backup.schedule is the same value morzer init --backup-schedule takes, and until it was a setting it could not be changed afterwards at all. Changing it rewrites the timer unit; unsetting it returns the default rather than turning backups off, because an empty OnCalendar is a unit systemd refuses to load.

Both schedules are checked with systemd's own parser before they are stored — by config set, and by init --backup-schedule before anything is created — so an expression systemd cannot read is refused rather than left as a timer that never loads. On a host without systemd-analyze there is nothing to ask, and the value is stored as given.

Turning scheduled backups off

Two ways, and they are not the same thing.

morzer config set backup.scheduled=false   # this machine's backups are not morzer's job
systemctl disable --now demo-backup.timer  # this timer, on this machine, stops

The setting decides whether the timer exists. Declaring it removes the backup service and timer, and morzer doctor stops asking how old the last backup is — you have told it the job belongs to something else, which is a declaration it can read rather than a fact it would be guessing at. It travels with the installation, so morzer init --repair reproduces it and installation describe records it.

systemctl disable decides whether an existing timer runs, and it now sticks: reconciliation no longer re-enables a unit that already exists. Only morzer init --repair --install-units switches it back on, which is the one command whose job is to put a machine right. doctor reports a unit you disabled as not enabled, and names both ways out of that.

systemctl mask does not work on a generated unit, and that is settled rather than pending. A mask is a symlink to /dev/null at /etc/systemd/system/<unit>, and that is exactly where morzer writes the unit, so the path is already occupied. The units live there deliberately: systemd loads /etc/systemd/system in preference to /usr/lib/systemd/system, so the file morzer writes is the one that takes effect, and moving them elsewhere would leave an older copy silently winning on every machine that already has one. Use either of the two above — between them they say everything a mask would.

Taking a backup by hand is unaffected by both: morzer backup still works, and a backup that exists is still expected to reach a target.

Settings and parameters are set in separate commands. They run on different machinery — one converges a deployment, the other writes a flag — so a mixed command is refused rather than half-applied.

It re-creates, it does not restart

A published port is fixed when a container is created. docker compose restart restarts the existing containers, so a restart after a port change would report success and leave the old mapping in place. config set re-creates the affected services instead.

This is the opposite of secret rotate, which restarts: a secret reaches a container as a mounted file, and restarting re-reads it.

A parameter that declares no dependent services is recorded and takes effect on the next apply. The summary says which of the two happened, and says it in the tense it happened in — a --dry-run never claims a service was re-created.

A value an older release left behind

A release that drops a parameter leaves its recorded value bound to nothing. That is the vendor's decision, so it is reported rather than refused: config list marks it stale, and config unset <name> clears it.