Spec schema
Desired-state contracts. Runtime fields (status, active_requests, last_access, backend_addr) are never part of these specs.
AppSpec (app.yml)
Section titled “AppSpec (app.yml)”apiVersion: momo.dev/v1kind: Appmetadata: id: api # DNS-label: ^[a-z][a-z0-9-]{0,62}$placement: node: node-a # or automatic placement: # selector: # matchLabels: # zone: eastroutes: - domain: api.example.com target: container: web # must exist in containers port: http # port name or declared numeric portcontainers: - name: web image: ghcr.io/acme/api:1.2.3 command: ["my-server"] # optional args: ["--port", "8000"] # optional working_dir: /app # optional user: "1000:1000" # optional ports: - name: http container: 8000 publish: loopback-dynamic # or omit / none expose: [9000] # optional, not published to host env: DATABASE_URL: postgres://... volumes: - name: db-data type: named # named | bind target: /var/lib/data # bind mounts require cluster policy allow_bind_mounts: # - name: uploads # type: bind # source: /srv/uploads # target: /uploads networks: - name: app-net aliases: [api] labels: team: platform resources: memory_mb: 512 cpu: 1.0 pids: 512 health: http: path: /health port: http # or: tcp: { port: postgres } dependencies: - container: db condition: running # running | tcp | http idle: keep_warm # optional restart_policy: unless-stopped # no | always | unless-stopped | on-failure update_policy: recreate # recreate | restart idle_timeout_secs: 300 # optional; node default if omitted startup_timeout_secs: 60 # optional; node default if omittedupdate_policy: recreate (default) rebuilds the Docker container when the create-shape fingerprint changes. The node stores that fingerprint as Docker label momo.spec_fp at create time (sha256 of image, command/args, ports, env, volumes, networks, resources, privileged/host_network/cap_add/devices, and user labels). Reconcile compares the label to the desired spec — it does not round-trip Docker inspect fields. Image retarget always recreates. Mutable restart_policy uses a Docker update. update_policy: restart ignores create-shape drift (except image).
Unknown top-level fields are rejected. Duplicate container or port names are rejected. Route targets must resolve to an existing container and port.
See examples/momo/app.yml in the repo.
Unsafe Docker options (rejected by default)
Section titled “Unsafe Docker options (rejected by default)”| Option | Policy flag on ClusterSpec.policies |
|---|---|
| Bind mounts | allow_bind_mounts |
| Privileged mode | allow_privileged |
| Host network | allow_host_network |
| Docker socket mounts | allow_docker_socket (also needs bind mounts) |
| Device mounts | allow_devices |
cap_add |
allow_raw_capabilities |
raw_host_config |
allow_raw_host_config |
All policy flags default to false.
HostSpec
Section titled “HostSpec”Generated per node from assigned apps. Stored under file://…/hosts/<node-id>.json.
| Field | Description |
|---|---|
node_id |
Owning node |
revision |
Monotonic; bumps on effective change |
apps[] |
Assigned apps with containers + route target metadata |
networks[] |
Required Docker networks |
volumes[] |
Required named volumes |
No-op applies do not bump revision unless --force-revision.
NodeSpec
Section titled “NodeSpec”Registered via momo node link --node-id … --grpc-endpoint … --store ….
| Field | Description |
|---|---|
node_id |
Unique cluster identity |
grpc_endpoint |
Gateway dial address |
labels |
Optional labels |
drain |
active or draining (draining rejects new placements) |
capacity |
Optional capacity hints |
RouteSpec / ClusterSpec
Section titled “RouteSpec / ClusterSpec”ClusterSpec holds revision, nodes[], routes[], and policies.
Routes map domain → app_id + target + node_id. Duplicate domains are rejected. Gateway Host lookup normalizes case and strips an optional port (api.example.com:443 → api.example.com).
File store
Section titled “File store”URI: file://./momo-state (development / single-machine JSON tree).
momo-state/ cluster.json apps/<app-id>.json hosts/<node-id>.json status/nodes/<node-id>.json # observation heartbeat (not desired state) audit/audit.jsonl # mutating CLI actionsWrites are atomic (temp + fsync + rename). Corrupt files fail validation and do not replace the last good revision.
MOMO_ENV=production refuses file stores unless MOMO_ALLOW_FILE_STORE=1.
SQL stores
Section titled “SQL stores”| URI | Use when |
|---|---|
sqlite://./momo.db |
Single-host multi-process (CLI + gateway + nodes on one machine). WAL mode + poll watches. |
postgres://user:pass@host/db / postgresql://… |
Multi-host shared store. LISTEN/NOTIFY + poll catch-up. |
Both SQL backends share the same logical schema (cluster, apps, hosts, node_status, audit_log), transactional mutate with optimistic concurrency on cluster.revision, and migrate-on-open. Concurrent conflicting writers receive momo_conflict.
Watch lag is exported as Prometheus gauge momo_store_watch_lag_seconds.
Placement & scheduling
Section titled “Placement & scheduling”- Explicit
placement.nodeor CLI--nodewins when the node is linked and not draining. placement.selector.matchLabelspicks among Active nodes with matching labels (least-loaded by app count; capacity hints break ties).- Decision is audited and shown in
momo inspect/ status JSON.
Cluster policies
Section titled “Cluster policies”Unsafe Docker options default off (allow_bind_mounts, allow_privileged, …). reject_plaintext_secret_env defaults on — env keys matching PASSWORD/SECRET/TOKEN/API_KEY/PRIVATE_KEY with non-empty values are rejected unless the policy is disabled.
Operator commands
Section titled “Operator commands”momo validate -f app.ymlmomo node link --node-id node-a --grpc-endpoint http://127.0.0.1:50051 \ --label zone=east --capacity cpu=8 --store file://./momo-statemomo apply -f app.yml --node node-a --store file://./momo-statemomo doctor --store file://./momo-state --gateway-url http://127.0.0.1:8080momo export --store file://./momo-state -o backup.yamlmomo gateway --store file://./momo-stateJSON success envelopes include schema_version: "momo.cli.v1". See also CLI reference.