maand build

Build reads your workspace from disk, updates the bucket database (maand.db), fills the in-memory KV store (then persists it), and generates TLS material. It does not contact workers.


CLI

maand build [--purge-secrets-kv]
Flag Description
--purge-secrets-kv Force purge vars/job/<job> and secrets/job/<job> for workspace jobs with no active allocations
(default) Full workspace reconcile (no job or worker filter)

Prerequisites

  1. maand init in the bucket directory (creates DB schema, default maand.conf, CA, SSH key, tmp/).
  2. CGO_ENABLED=1 when compiling maand (SQLite driver).
  3. Host tools for post_build hooks: python3, bun (if using Bun commands), bash, ssh, rsync.
  4. Valid workspace/workers.json and at least one job under workspace/jobs/<name>/ with manifest.json and Makefile.

Workspace inputs

workspace/workers.json

Array of workers (hosts are SSH targets from the maand CLI host):

[
  {
    "host": "10.0.0.1",
    "labels": ["worker", "gpu"],
    "memory": "4096 mb",
    "cpu": "2000 mhz",
    "tags": { "zone": "a" }
  }
]

Optional fields are omitted from the file when unset (no null values).

Workers removed from workers.json are marked removed on allocations (not deleted until deploy/GC).

workspace/jobs/<job>/manifest.json

{
  "version": "1.0.0",
  "selectors": ["worker"],
  "max_concurrent_upgrades": 2,
  "max_concurrent_starts": 0,
  "resources": {
    "memory": { "min": "128 mb", "max": "512 mb" },
    "cpu": { "min": "100 mhz", "max": "500 mhz" },
    "ports": { "http_port": {} }
  },
  "hooks": {
    "hook_setup": {
      "executed_on": ["post_build", "pre_deploy"],
      "demands": {
        "job": "other_job",
        "hook": "hook_base",
        "config": {}
      }
    }
  },
  "certs": {
    "tls": {
      "pkcs8": false,
      "one": true,
      "subject": { "common_name": "app.example" }
    }
  }
}
Field Purpose
version Job version string — see Job version below
selectors Worker labels for placement. When omitted, the job name is used (all selectors must match worker labels).
max_concurrent_upgrades Rolling restart batch size during deploy upgrades (minimum 1; default 1).
restart_policy Default always. reload runs make reload on upgrades; never rsyncs without lifecycle.
restart_globs Only with reload. Changed paths matching a glob run restart instead of reload.
max_concurrent_starts Rolling start batch size on first deploy (0 = all new allocations at once).
min_allocations_count Minimum non-removed allocations after placement (0 = no minimum). Build fails when fewer workers match.
resources.memory / cpu Min/max bounds in manifest; actual reservation from bucket.jobs.conf or bucket.jobs.<env>.conf (job_config_selector in maand.conf) — resources-and-placement.md.
resources.ports Named ports: {} (maand assigns from pool) or integer (fixed; any port number)
hooks Named commands (must be prefixed hook_).
certs Per-job cert definitions → generated into KV per worker.

Job version

Each job may declare version in manifest.json. Build stores it in job.version and KV (maand/job/<job>/version). When omitted, the target version is stored as 0.0.0.

Per-allocation target version (allocations.new_version) is set on maand build from manifest.json. Running version (hash.current_version) is updated on deploy promote — see deploy.md.

Format (validated when the field is present or required):

When version is required

Job role Rule
Standalone job (no demands, not demanded) Optional — omit allowed
Job with demands.job set on any command Must declare version
Upstream job referenced by another job’s demand Must declare version

Version constraints on dependencies

In demands.config, optional bounds are checked against the upstream job’s version at build:

"demands": {
  "job": "database",
  "hook": "hook_schema",
  "config": {
    "min_version": "2.0.0",
    "max_version": "3.0.0"
  }
}

Full rules: manifest.md · deployment-sequence.md

Job directory rules

Health check (build validation)

A job may define manifest probes, a health_check command, or both:

When both are defined, manifest probes run first at check time, then commands. Manifest probes are stored in job.health_check (JSON). Command-based health is stored in hooks like other events. See health-check.md.

workspace/disabled.json (optional)

How-to: disable-and-drain.md.

{
  "jobs": {
    "myjob": {
      "allocations": ["10.0.0.1"]
    }
  },
  "workers": ["10.0.0.2"]
}

Disabled allocations are not active for deploy lifecycle (start/restart/reload/rsync) but still receive hook fan-out and deploy staging; rows remain until removed from workspace/build.

workspace/bucket.jobs.conf (optional)

Per-job reservations for memory and CPU in the current environment. Values must fall within manifest min/max (resources.memory / resources.cpu).

[myjob]
memory = "256 mb"
cpu = "500 mhz"
job_config_selector in maand.conf File used
"" bucket.jobs.conf
"prod" bucket.jobs.prod.conf

See configuration.md and resources-and-placement.md.

workspace/bucket.conf

Bucket-wide settings (TOML key/value). Created on maand init with a default port pool:

port_range = "30000,39999"

When port_range is omitted or empty in an existing file, build still uses 30000,39999.

Job manifests declare named ports in two ways:

Assigned numbers are stored in ports and exposed in KV namespace maand/bucket (key <port_name>). Maand-provisioned ports keep the same number across rebuilds while it remains inside port_range. Changing a port from a fixed integer to {} reassigns from the pool when the stored number is outside that range (for example fixed 9500{} with default pool 30000,39999). Fixed ports always follow the manifest value.

"resources": {
  "ports": {
    "database_port": 5432,
    "http_port": {}
  }
}

Port names must be lowercase identifiers prefixed with <job>_ (for example api_http_port, echo_server_http) so keys stay distinct in the shared maand/bucket KV namespace. The same port number cannot be used by two jobs or two port names in the bucket.

Other keys in bucket.conf are copied to KV namespace vars/bucket.

maand.conf

Bucket-root SSH and cert settings. Full field reference: configuration.md.


What build does (order)

All steps run in one transaction (except post-build hooks), then VACUUM:

Step Function Summary
1 kv.Initialize Load KV from DB into memory.
2 BuildWorkers Sync workers.jsonworker, labels, tags; drop removed workers from catalog.
3 BuildJobs Sync each job manifest → job, job_selectors, hooks, job_files, ports, job_certs.
4 ValidateHookDemands Verify demand job/hook refs; parse version; check min_version / max_version.
5 BuildAllocations Label-match jobs to workers → allocations rows (alloc_id, deployment_seq initially 0).
6 BuildDeploymentSequence Compute deployment_seq from command demands (dependency order).
7 BuildVariables Populate KV namespaces (workers, jobs, bucket vars, job/allocation metadata).
7b checkCAExpiry Read secrets/ca.crt; warn on stderr when CA is expiring or invalid PEM; fail when CA is expired (before BuildCerts).
8 BuildCerts Regenerate leaf certs when CA/manifest changed or within renewal window; write cert PEMs into KV.
9 BuildJobAllocationVariables Per-allocation keys (*_allocation_index, peer_workers) after certs so cert sync does not delete them.
10 BuildPrometheusCatalog When a prometheus server job exists (prometheus.yml or .tpl): validate all _prometheus/ (alerts, runbook refs, scrape shape); write scrape-only KV. Jobs with maand:port/* targets and no active allocations are omitted from aggregate scrape_jobs / scrape_configs but keep per-job scrape/<job> KV. Without a prometheus server job, scrape KV is cleared and _prometheus/ validation is skipped.
11 PurgeStaleVersions Trim old KV versions (keep 7 per key).
12 ValidateWorkerResources Ensure allocated jobs fit worker memory/CPU.
13 PersistToTransaction Write KV changes into key_value table.
14 Commit Persist catalog.
15 runPostBuildHooks Separate transaction; runs post_build commands, then persists vars/job KV; failures fail the build.

Deployment sequence (deployment_seq)

Derived from hooks.demand_job edges. See deployment-sequence.md for the full reference.

Summary:

Deploy runs sequences 0 .. max in order so depended-on jobs deploy first. post_build hooks use the same order.

Allocations

KV namespaces (build output)

Build populates catalog-backed namespaces (maand/*, vars/bucket/*, worker and job metadata, certs). Stable app config lives in vars/job/<job> and secrets/job/<job>.

Full namespace reference: KV namespaces. Persistence and purge: KV persistence.

Certificates

See certs.md for the full guide (CA, manifest options, auto-rotation, deploy paths). Summary:

post_build hooks

After the main commit, build runs every command registered with executed_on containing post_build, in deployment sequence order on non-removed allocations (includes disabled) on the CLI host (same runtime as deploy hooks).

Resource validation

If allocated jobs require memory or CPU (resources.memory / resources.cpu in the manifest), each worker hosting those jobs must declare memory / cpu in workers.json. Build fails when requirements exceed capacity or when a worker omits capacity while jobs require it.

When min_allocations_count is set on a job manifest, build fails with ErrInsufficientAllocations if label matching produces fewer non-removed allocations than the minimum.


Database tables touched

worker, worker_labels, worker_tags, job, job_selectors, hooks, job_files, ports, job_certs, allocations, hash, key_value, bucket, schema_version.

Inspect with:

maand cat workers
maand cat jobs
maand cat allocations
maand cat hooks
maand cat certs
maand cat kv

Common errors

Error Typical cause
ErrInvalidWorkerJSON Duplicate host, bad memory/cpu format
ErrInvalidManifest Bad resources, missing Makefile
ErrInvalidHookDemand Unknown demand job/hook or partial demand pair
ErrInvalidJobVersion Invalid or missing version on dependency participant
ErrHookDemandVersionMismatch Upstream version outside demand min/max
ErrPortKeyFormat Invalid port name in manifest
ErrInvalidManifestPort Port value is not {} or a valid integer in range
ErrInvalidPortRange Bad port_range in bucket.conf (must be "min,max")
ErrPortRangeExhausted No free ports left in the pool
ErrCircularHookDependency Demand cycle between jobs
ErrInsufficientAllocations Job has fewer non-removed allocations than min_allocations_count
bucket CA certificate (secrets/ca.crt) expired on … Replace secrets/ca.crt / ca.key — see certs.md
Worker resource validation Job memory/CPU exceeds worker capacity, or worker missing memory/CPU when jobs require it

When to run build

Build does not start or restart processes on workers; that is deploy.