Configuration
Schema version 1 project configuration for mm.
mm discovers the shared project file, .config/mm.yaml, starting at --root
(or the current directory) and searching its parents. Any top-level key except
schema-version can instead live in its own file under .config/mm/ (see
split a large configuration). If present,
.config/mm.overrides.yaml beside that file supplies checkout-local settings.
The current schema is version 1:
schema-version: 1
Unknown fields are errors throughout the typed schema. YAML must be valid UTF-8, each configuration file must be a regular file no larger than 1 MiB (4 MiB across all of them), and neither they nor configured project paths may contain symlink components. On Unix, files mm edits must be owned by the current user and not writable by group or others; project directories have the equivalent ownership and write restrictions.
Paths in configuration are project-relative unless a field explicitly says
otherwise. Absolute paths and .. traversal are rejected. Compose files and
module directories must exist. Build contexts and Dockerfiles are relative to
their module; build.extra-inputs are relative to the project root. A
development mount target is instead an absolute POSIX container path.
Local overrides
Create .config/mm.overrides.yaml with just the settings to change locally:
build:
platforms: [linux/arm64]
The override uses the same field names as the shared file. Maps merge recursively;
scalars and lists replace the shared value; omitted keys inherit it. An empty
map leaves inherited keys intact. null is an explicit value: it clears nullable
fields, such as default-environment, and is invalid for non-nullable fields.
Use {} for an empty override file. The shared configuration must remain complete
and valid on its own; defaults are applied when deserializing the merged result.
Both sources use schema version 1, and the override need not repeat schema-version.
All project commands use the effective configuration, including validation,
builds, deployment and setup inspection. Existing CLI and private target/runtime
preference precedence still applies. Paths keep their usual project-relative
meaning. An invalid override is an error, rather than a fallback to shared values.
Changing either source requires a new review for configuration-dependent plans.
Config-editing commands write the shared file that declares the key, either
.config/mm.yaml or its split file; a local override still takes precedence
over an edited shared setting.
mm init includes /.config/mm.overrides.yaml in the project .gitignore as
part of its reviewed setup. Existing projects should add and commit this rule
once. Keep the override untracked. See local build platforms.
Split a large configuration
When one key grows large, or needs its own reviewers, move it to its own file.
mm config split reviews the move first, like other config edits:
mm config split stacks # prints the plan and its hash
mm config split stacks --plan HASH --yes # applies exactly that plan
mm config sources # shows where each key lives
Expected result: .config/mm/stacks.yaml holds the value of stacks, with its
comments, and .config/mm.yaml no longer has a stacks: key. The plan is
refused unless the project means exactly the same thing before and after the
move. mm config join stacks moves the key back.
A split file holds only the key's value, without the key itself:
# .config/mm/stacks.yaml
app:
files: [compose.yaml]
You can also create or edit these files by hand. The rules:
.config/mm.yamlis still required. mm finds the project by it, and it keepsschema-version. Every other top-level key can be split, includingproject.- Each key lives in exactly one place. A key in both
.config/mm.yamland its split file is an error that names both; nothing is merged. - File names are the exact key plus
.yaml, such as.config/mm/environment-delivery.yaml. Any other file, a.ymlextension, a subdirectory, or a symlink in.config/mm/is an error. Hidden files and names ending in~are skipped, so editor swap files don't break loading. - An empty split file is an error. Write
{}for an empty map, or delete the file. - Paths inside a split file are relative to the project root, as in
.config/mm.yaml. Errors name the split file and the line. .config/mm.overrides.yamloverlays the combined result, as before.- Commit split files. Remote environments load configuration from a commit, so
mm config validatewarns (MM_CONFIG_SOURCE_IGNORED) when git ignores one. - Splitting changes the configuration fingerprint, so plans reviewed before the move must be reviewed again. Projects that split nothing keep their existing fingerprint.
To give a key its own reviewers, list its file in CODEOWNERS:
/.config/mm/environments.yaml @acme/platform
Editors can validate a split file with its own schema. mm config schema KEY
prints it, and the docs publish one per key at /schema/mm/KEY.schema.json,
for example /schema/mm/stacks.schema.json.
Top-level fields
New to the file? Start with where configuration belongs or a runnable example. The sections below are a field reference; YAML fragments show the relevant settings and are not complete projects unless noted.
| Field | Required | Shape and default |
|---|---|---|
schema-version | yes | Integer 1. Missing, zero, and every other value are errors. |
project | yes | {id, name, development-environment?}. id is a UUID. |
default-environment | no | Environment selected when neither --environment nor MM_ENVIRONMENT is set. |
target | yes | Default target name. |
runtime | no | {driver?}; driver is podman-compose, docker-compose, or docker-swarm. |
build | no | {platforms, tag-strategy}; defaults to linux/amd64, linux/arm64, and auto. immutable is also valid. |
networks | no | Map of named empty objects. Stacks and modules attach names from this catalog. |
stacks | yes | Map of Compose stack definitions. |
modules | no | Map of source modules and their services. |
secrets | no | Map of logical secret declarations; never put secret material in this file. |
bindings | no | Project-wide environment bindings. |
environment-delivery | no | {default?, development-default?} using direct or env-file. |
environments | yes | Map of environment overlays; at least one is required. |
ingress | no | Project automatic-local and middleware policy. |
development-mounts | no | {sensitive-patterns: [...]}. |
development-mail | no | {enabled?, bindings?}. |
jobs | no | Map of named one-shot command declarations. |
schedules | no | Map of durable five-field cron triggers referring to jobs. |
gates | no | Map of declarative provider and ingress references. |
users | no | Who may reach each remote environment. See remote access. |
agents | no | What an AI agent may do, per environment or *. Default view. See agents. |
targets | no | Remote targets: {host, port?, host-key, daemon-fingerprint, members?} or {nodes, swarm, setup?}. See the remote daemon guide. |
registries | no | Image registry logins by host: {username, password: {secret}}. See prepare a deployment environment. |
Names used for projects, stacks, modules, services, networks, jobs, schedules,
and gates contain only ASCII letters, digits, -, and _. More restrictive
engine-specific checks also apply where relevant.
Stacks and ingress
networks:
frontend: {}
stacks:
app:
project-name: myapp-{environment}-{stack} # optional; {project-id} also works
files: [compose.yaml, compose.local.yaml]
networks: [frontend]
shared-volumes: [uploads] # Swarm: every node can mount these
ingress:
- domain: app
route: / # default
service: web # optional when unambiguous
port: 8080 # optional
https: true # default
environments: [local, staging] # empty means all
ingress:
automatic-local: true
middlewares:
redirects:
file: .config/traefik/redirects.yaml
environments: [local]
ingress under a stack accepts either one route mapping or a list. Domain keys
resolve through the selected environment's domains. Route paths start with
/, contain no traversal or repeated slash, and have no trailing slash except
for /. Middleware files are bounded, project-relative regular files; the
currently supported document shape is http.middlewares with
replacePathRegex definitions.
shared-volumes lists top-level Compose volume names from that stack. On a
multi-node Docker Swarm, mm stores them on its built-in shared storage so a
service can run on any node and see the same files. Do not set driver,
driver_opts, or external on them; mm sets those. Other runtimes and
single-node Swarm treat them as ordinary volumes. See
Run on a multi-node Swarm.
Modules, services, and builds
modules:
api:
path: apps/api
networks: [frontend]
env:
template: .env.example
build:
enabled: true # default
context: . # optional
dockerfile: Dockerfile # optional
image: ghcr.io/acme/api # required when enabled
default-variant: production
targets: { production: "", debug: debug }
args: { APP_MODE: production }
secrets: { npm_token: npm.token }
credential-only-secrets: [npm_token] # optional
depends-on: { base: MM_BASE_IMAGE }
extra-inputs: [package-lock.json]
services:
web:
stack: app
role: http
http-port: 8080
automatic-local-ingress: true
local-https: true
development-mail: true
networks: [frontend]
placement: edge # Swarm only: node, group, `managers`, or a list
env:
APP.DEBUG: false
secret-files:
database-password:
secret: database.password
uid: "0" # defaults shown
gid: "0"
mode: 292 # YAML integer for 0444
Service roles are http, database, queue, worker, scheduler, and
job. Infrastructure and background roles do not receive automatic ingress.
placement chooses where a service runs on a multi-node Docker Swarm. Use a
node hostname (mgr-1), a placement group created with
mm target swarm group set, managers, or a list whose entries must all hold.
Compose runtimes and single-node Swarm ignore it, so the same file works on a
laptop. On a cluster, a service that keeps data in a non-shared named volume
must be placed on exactly one node.
Set environments on a service to run it only in the named environments. Omitted
or empty lists mean all environments; every name must be configured:
modules:
api:
services:
scheduler:
stack: app
role: scheduler
environments: [integration, uat, production]
For other environments, mm omits that service from the generated Compose files, environment/secret delivery, managed networks and automatic ingress. Build and image declarations remain module-level; a deployment needs published images only for modules with active services. Deployment and stack-compilation plans list excluded services. Applying a deployment removes previously deployed excluded services; a stack with no remaining services is removed as a whole.
Active services cannot depend on excluded ones through depends_on, links,
volumes_from, or service: namespace sharing (network_mode, ipc, pid).
Optional depends_on entries with required: false are removed. Dependency
checks apply to each source Compose file; remove conflicting references from
earlier files as well as later overrides. Explicit ingress routes must allowlist
compatible environments. Jobs and schedules targeting an excluded service are
inactive in that environment.
Build args are public values; names that look secret are rejected and belong
under build.secrets.
credential-only-secrets lists keys of build.secrets that only authenticate
fetching inputs, such as a package registry token. They are still mounted, but
they are left out of the build fingerprint, so they do not prevent reusing an
unchanged image or promoting a build to another environment. Any other build
secret disables both. See build secrets.
Jobs and schedules
jobs:
migrate:
module: api
service: web
command: ["php", "artisan", "migrate", "--force"]
environment: production # optional fixed environment
timeout: 10m
retry: { attempts: 3, backoff: 5s, max-backoff: 5m }
output-limit-bytes: 65536
history-limit: 100
schedules:
framework:
job: migrate
cron: "0 2 * * *"
timezone: America/New_York
enabled: true
starts-at: 2026-01-01T00:00:00Z
ends-at: 2027-01-01T00:00:00Z
jitter: 30s
concurrency: forbid # allow, forbid, replace
misfire: run-once # skip or {bounded-catch-up: {maximum: 10}}
retry: { attempts: 2, backoff: 10s, max-backoff: 1m } # optional override
timeout: 5m # optional override
Durations use one compact suffix: ms, s, m, h, or d. Commands are
argument vectors and never shell strings. A job reuses the named service's
image, environment, networks, mounts, and secret delivery contract. See
Jobs and durable schedules for timing, recovery,
and operational semantics.
A module can also contain development-mount:
development-mount:
source: .
target: /workspace
mode: read-write # or read-only
services: [web]
preset: node # laravel, node, dotnet, or rust
preserve: [{ path: /workspace/node_modules }]
excludes: [dist]
protected: [.env]
safe-exceptions: []
Bindings, environments, and secrets
Binding keys use dot-separated uppercase names such as DB.HOST. A binding is
exactly one of these untagged shapes:
bindings:
APP.DEBUG: { value: false }
DB.PASSWORD: { secret: database.password }
APP.URL: { cast: url, value-ref: domains.app }
environments:
local:
domains: { app: app.example.test }
bindings:
APP.DEBUG: { value: true }
runtime: { driver: podman-compose }
target: local
build-variant: debug
environment-delivery: env-file
automatic-local-ingress: true
Environment bindings override project bindings. direct passes
operation-scoped interpolation values to the runtime; env-file uses a private,
operation-scoped file. With a declared development environment, the built-in
default is env-file for that environment and direct elsewhere. Explicit
environment, development, and general policy values override those defaults.
providers:
staging-secrets:
driver: openbao
scope: project
mode: external
address: https://bao.example.test:8200
storage-mount: mm-kv
automation-auth-mount: mm-approle
trust-bundle: company-ca.pem
expected-trust-sha256: sha256:HEX_DIGEST
secrets:
database.password:
storage:
provider: staging-secrets # age, env, or a declared provider instance
location: projects/example/db
input: { prompt: Database password }
rotation:
strategy: manual # manual, replace-and-redeploy, coordinated
interval-days: 90
app.key:
storage: { provider: age, location: app.key }
generate: { preset: bytes, length: 32, encoding: base64-url }
Generator presets are password (length 32), token (48), bytes (32 and
base64-url), laravel-app-key, and uuid. Length overrides must be 16–4096.
Other byte encodings are base64 and hex. Provider instance names are stable
and independent of driver/mode. Credentials and recovery material remain
private mm state, not project configuration.
Development mail, jobs, schedules, and gates
development-mail:
enabled: true
bindings:
MAIL_HOST: smtp-host
MAIL_PORT: smtp-port
MAIL_URL: smtp-url
MAIL_UI: ui-url
jobs:
migrate:
module: api
service: web
command: [php, artisan, migrate, --force]
environment: staging # optional
schedules:
nightly-migrate:
job: migrate
cron: "0 2 * * *"
enabled: true # default
gates:
admin:
provider: password-cap
ingress: [admin]
Jobs require an existing module/service and a non-empty argument vector.
Schedules require an existing job and exactly five cron fields. A gate
declaration names a gate driver such as password-cap and optional ingress
references. Operational policies, users, sessions, and credentials are private
state rather than project configuration. The --secret-provider accepted by
mm access add separately selects storage for access secrets; it is not this driver
field.
Schema and validation
mm config schema prints the generated JSON Schema without loading a project or
probing a container engine:
mm config schema > mm.schema.json
mm config validate
The same generated schema is published as
/schema/mm.schema.json, with one schema per
top-level key for split files under /schema/mm/. Core tests compare that file
byte-for-byte with mm config schema, while the hand-written sections above explain
cross-resource behavior that JSON Schema alone cannot express.
./scripts/generate-artifacts.sh is the only checked-in generation path. The
schema carries its source and generation command and must not be manually edited.
Schema 1 is the complete current project contract. Missing or different schema versions are rejected rather than guessed or rewritten.
mm config validate resolves the selected environment and performs cross-resource,
path, file, ingress, build, environment, and secret-reference checks without
inspecting containers.