Skip to main content

Rigs (Docker compose lifecycle)

ign rig manages a compose rig — a Docker compose project running an Ignition gateway plus its satellites. It shells out to docker compose (the v2 plugin line; compose ≥ v2 required — the legacy docker-compose v1 binary is not supported and a missing/old compose fails fast with exit 7 and an install hint). Every operation starts with a one-shot resolve run (docker compose -f <file> --project-directory <dir> config --format json); the resolved project .name — which honors the rig's own .env COMPOSE_PROJECT_NAME — is the identity truth, and every later op passes it as an explicit -p <name> (no implicit directory-name projects, ever).

Rig discovery (in this order)​

LevelSourceNotes
1--rig NAMElooks up [rigs.NAME] in the config; unknown name → exit 7 with the known rigs listed
2IGNITION_RIG envsame lookup (folded into the flag — one env→flag home)
3[rig].defaultthe config's explicit preference — BEATS the cwd scan; a stale default is a loud exit 7
4cwd compose files./docker/compose.yml, ./docker/docker-compose.yml, ./compose.yml, ./compose.yaml, ./docker-compose.yml
5WHK conventionsignition-git-module/docker/docker-compose.yml, then whk-environment-orchestration/docker-compose.yml — each probed under BOTH ~/Documents/whiskeyhouse/ and ~/whiskeyhouse/ (first hit wins)

Config surface:

[rig]
default = "git-module"

[rigs.git-module]
compose_file = "~/Documents/whiskeyhouse/ignition-git-module/docker/docker-compose.yml"
# project_name optional — omit to honor the rig's own .env

compose_file expands ~ and ${VAR}. IGNITION_RIG_ROOTS (path-separated) overrides the convention home roots — for machines whose WHK checkouts live elsewhere (and for test isolation). Nothing found → exit 7 with the full search trail in the message.

The docker-only contract: profile: null​

Rig verbs are the first commands with no gateway dependency: no profile, secret, or client is resolved, and the envelope echoes profile: null on both success and error. --profile has no effect on rig.

rig up semantics​

  1. compose version gate (fail fast, exit 7 + install hint);
  2. port pre-flight — a host port held by ANOTHER project's container aborts before anything starts: port 9088 in use by container X (rig Y) (same-project occupants are recreate-safe; a non-docker host process is attributed via lsof when available);
  3. up -d --wait --wait-timeout <N> --remove-orphans;
  4. commissioned wait: the rig's own gateway port is derived from the resolved mappings (first target 8088 → http://localhost:<pub>, else target 443 → https) and its /StatusPing is polled header-less until RUNNING — same budget as --timeout.

Uncommissioned is data, not failure. A fresh-volume gateway terminally 302s to /welcome — there is no headless commissioning (verified: no commissioning endpoints exist). rig up then exits 0 with state: "uncommissioned" and the wizard URL inside warnings (the version-command degradation precedent). A deadline reached while still STARTING is a real exit-7 failure.

{"ok": true, "profile": null, "data": {"rig": "ignition-devops", "project": "ignition-devops",
"state": "uncommissioned", "gateway_url": "http://localhost:9088",
"warnings": ["gateway uncommissioned — open http://localhost:9088/welcome in a browser and complete the commissioning wizard (no headless commissioning exists)"]}}

Rig modules — opt-in, signed, never automatic​

A rig can declare modules to provision. Nothing is installed unless you ask for it: a rig with no modules table behaves exactly as it always has.

[rigs.dev]
compose_file = "docker/compose.yml"
# Optional: only needed when the gateway service cannot be derived from
# the compose file (ign picks the first service publishing a port
# targeting 8088, then 443).
# module_service = "gateway"

[rigs.dev.modules.git]
version = "2.3.4"

[rigs.dev.modules.project-scan-endpoint]
version = "1.0.0"

Registered modules:

Registry idSource repositoryGateway module idMin gateway
gitWhiskeyHouse/ignition-git-modulecom.axone_io.ignition.git8.3.1
project-scan-endpointbw-design-group/ignition-project-scan-endpointproject-scan-endpoint8.3.0

The two rows differ in every column — including the gateway module id's shape (reverse-DNS vs bare slug), which is why it is a separate field rather than derived from the registry id. Only signed release artifacts are ever installed; ign will not build, sign, or fetch an unsigned module.

Trying one without editing config. --with-module ID@VERSION on rig up and rig reset is repeatable and additive for that invocation only. A flag wins over a config declaration for the same id. There is deliberately no subtractive form — you cannot run without a config-declared module; remove it from config instead.

ign rig up --with-module project-scan-endpoint@1.0.0

Fetch policy. A verified artifact is cached by version, and a cache hit makes no network request. Two flags change that:

FlagEffect
--refreshRe-checks the feed for the pinned version. If upstream now publishes a different digest than the one cached, this REFUSES (exit 6) and names both digests; the cached artifact is left untouched.
--accept-upstream-changeAccepts a changed upstream digest and caches the new bytes — which are still verified against the newly published digest. Never implied by --refresh: accepting a re-released artifact is a deliberate act.

The override file. ign writes compose.ign-modules.yml next to the rig's compose file and passes it as a second -f. That file is ign's: it is regenerated whole on every provisioning run, never hand-merged. ign never writes to the compose file you wrote. Deleting compose.ign-modules.yml stops the module being mounted, and the next rig up with nothing declared removes the file for you.

It does not uninstall the module from the gateway. Ignition installs an accepted module into its own data directory, which lives in the rig's volume and outlives the mount — so a gateway that already loaded a module keeps reporting it after the override is gone (verified live, not inferred).

To remove it from the gateway, use the deliberate verb:

ign rig module uninstall git --yes

The id is a registry id — the same one --with-module and the config table take, not the gateway's own module id. The verb needs IGNITION_TOKEN, the same credential contract as rig snapshot and rig restore.

In the TUI it is the Rig screen's actions menu (a → module uninstall): a form takes the registry id, an unknown id is refused locally against the same registry, and a Confirm gate — the cockpit's --yes — states the irreversibility before the write goes out.

The order matters, and the gateway enforces it. Undeclare the module, run ign rig up so the mount goes, then uninstall. The gateway refuses to uninstall a module whose .modl is still mounted. Once it succeeds the effect is immediate — no gateway restart. ign refuses up front if the rig's config still declares the module, rather than letting you get a confusing failure back from the gateway.

This is not undone by re-declaring the module. A re-declared, re-mounted module does not come back — verified live. Recovery today means ign rig reset, which destroys the rig's whole data volume along with everything else in it. That makes this the most severe of ign's guarded operations: the others cost you a re-run, this one costs the volume. It refuses without --yes for that reason, and it is a separate verb rather than something ign rig up offers, because IGNITION_YES=1 would turn a mid-up prompt into no prompt at all.

When you stop declaring a module, ign rig up tells you. It drops the mount and deletes the override, then reports the module rather than removing it from the gateway:

$ ign rig up
...
orphaned: git (com.axone_io.ignition.git) — still installed on the gateway
remove with: ign rig module uninstall git --rig dev --yes

The same data is on the --json envelope, on an always-present key that is an empty list when there is nothing orphaned:

{
"orphaned_modules": [
{
"id": "git",
"gateway_module_id": "com.axone_io.ignition.git",
"remove_with": "ign rig module uninstall git --rig dev --yes"
}
]
}

rig up does not remove it for you, for two reasons that are not style preferences. IGNITION_YES=1 is a documented pattern for agents and CI, and it merges into --yes — so a confirmation asked mid-up would be no confirmation at all for exactly the callers least able to notice an irreversible act. And rig up needs no gateway credential today; performing a gateway write would change that contract for every user of the verb. Removal stays something you type.

The report comes from ign's own override file, not the gateway's module list. ign diffs what it provisioned last time against what you declare now, so it can only ever report modules it installed itself. A module you installed by hand is invisible to that diff and will never be named — which is the point: the report leads to an irreversible command, so it must not point at something ign did not put there.

Provisioning mounts a module; it does not commission one. No repository, credential, or module configuration is supplied — a provisioned module is present and accepted by the gateway, not set up.

rig status is an allowlist​

Status NEVER passes through docker compose config/inspect output — the resolved config contains GATEWAY_ADMIN_PASSWORD and friends. The JSON data is exactly {rig, project, compose_file, services[]: {name, state, health, exit_code, publishers[]: {published_port, target_port, protocol}}, volumes: [names], ports_free} — all keys always present (empty arrays when none). A down rig is exit-0 data (ports_free: true, empty services).

rig reset semantics (the no-stale-state contract)​

reset is the phase's destructive verb: it refuses without --yes (exit 2, confirmation_required, hint names --yes and IGNITION_YES=1) and the guard fires BEFORE any discovery runs — a refusal costs nothing (no docker, no config scan; binary-pinned by exiting 2 in a directory with no rig discoverable at all).

The cycle, in order:

  1. preview — docker volume ls label-filtered to the project: the removed_volumes array in the result data reports exactly what reset removes, as it acts;
  2. compose version gate (exit 7 + install hint when absent);
  3. down -v --remove-orphans — the LOCKED teardown: -v removes the project's named AND anonymous volumes (gateway data, trial state, everything), --remove-orphans kills renamed-service strays — down && up without -v is the classic stale-state anti-pattern reset exists to kill;
  4. port pre-flight with FRESH EYES — teardown frees the rig's own ports first; if another rig grabbed one mid-cycle, reset aborts with attribution and names the torn-down state in the hint;
  5. up -d --wait (the rig up invocation verbatim);
  6. commissioned wait — same semantics as rig up: a fresh volume usually terminally reports the wizard, which is DATA (exit 0, state: "uncommissioned", wizard URL in warnings); still-STARTING at the deadline is a real exit-7 failure.

The result data is {rig, project, removed_volumes, state, warnings} — all keys always. No project, trial, or gateway state survives the volume deletion; commission from scratch or restore a backup (04-04) afterward.

rig logs is passthrough​

rig logs streams docker compose logs output RAW: one line per stdout line, no envelope in ANY mode — compose log lines are not gateway JSON objects, and wrapping would corrupt them (rig logs --json is the same passthrough; contrast logs -f --json, whose entries ARE gateway NDJSON). --tail N (default 200) bounds the history; -f follows until Ctrl-C (default process kill, no envelope); an optional SERVICE positional filters to one service. Compose's own stderr diagnostics go to ign's stderr, never the data stream.

rig trial — state, and the reset ladder​

rig trial status is credential-free truth: both GET /data/api/v1/trial and GET /data/api/v1/overview/banners answer unauthenticated (live-verified on 8.3.3 and 8.3.6, expired AND active states), so a fresh rig with no provisioned token reports its trial state fine. The trial endpoint is the PRIMARY source (trialState ∈ {AllInDemo, SomeInDemo, NoneInDemo}, trialSecondsLeft in seconds, the expired flag); the trial banner is the cross-check — expireTime is epoch milliseconds or null, and an EXPIRED trial shows severity:"warning" + null (code expecting a future timestamp misreads expired as active), so banners.active is computed as severity=="info" AND expireTime>now_ms and never the reverse derivation.

rig trial reset runs the mechanism ladder (the spike-resolved native approach — browser delegation rejected: it needs Node+chromium, broke across 8.3.3's UI rewrite, and verifies via DOM text):

  1. tier 0 — token-auth POST POST /data/api/v1/trial with X-Ignition-API-Token (token from IGNITION_TOKEN; token mutations need no CSRF). One cheap call; on 2xx it wins.
  2. tier 1 — native gateway login (the live-verified mechanism, end-to-end on 8.3.3: expired:true → false, 0 → 7199s): the internal IdP's OIDC challenge dance (rotating tokens, ~4 captured cookies replayed by hand, webui-sid-<id> session cookie, csrfToken from /data/app/session) → the reset POST with the session cookie + X-CSRF-Token. Credentials: --user (or IGNITION_USER) + IGNITION_PASSWORD — the password NEVER rides a flag, env only.

Success REQUIRES the read-back flip: after a 2xx the trial is re-fetched and expired must be false — a bare 2xx never suffices (the mutation-reads-back discipline). The result data is {rig_url, mechanism: "token"|"login", expired_before, expired_after, trial_remaining_s}.

State gate (live-discovered): the gateway answers 403 to reset attempts on a NON-expired trial (verified from the browser page with the exact UI headers) — ign surfaces this honestly as exit 6 trial_not_expired naming the seconds left, instead of a misleading auth error. Wait for expiry (watch rig trial status) or rig reset --yes for a completely fresh trial volume.

Verification note: the reset path is verified end-to-end against 8.3.3 (the git-module rig, by hand during 04-03) and the login-flow machinery against 8.3.6 (ign-research; steps 1–5 + bad-credential shapes). Repeatable live gates ship as #[ignore] tests (cargo test -p ignition-core --test trial_contract -- --ignored with IGNITION_LIVE_URL + credentials + IGNITION_LIVE_MUTATIONS=1 against an expired rig). A tier-2 Playwright fallback exists ONLY as this documented env contract — ignition-trial-resetter / WHK-Global's e2e/reset_trial.mjs — and is never shipped as ign's mechanism.

rig snapshot / rig restore — repeatable state​

rig snapshot composes a rig's gateway state HONESTLY into one directory (default ./ign-rig-snapshots/<rig>-<yyyyMMdd-HHmmss>/, -o overrides; the stamp is std-only, no clock dependency):

  • <rig>.gwbk — the native roaming backup (GET /data/api/v1/backup?type=roaming, Accept: application/octet-stream), STREAMED to disk chunk-by-chunk (never buffered in memory — gwbks are tens of MB);
  • projects/<name>.zip — one export per runnable project (the project export machinery reused; file names percent-encode the project name injectively, so My Project → My%20Project.zip). The gwbk should already contain projects (postman semantics, MEDIUM confidence), but the explicit exports are the honest redundancy that makes the manifest truthful either way;
  • manifest.json — the composition record: {rig, taken_at (epoch s), ignition: {version}, gwbk, projects: [{name, file}], notes}. The notes name BOTH exclusions explicitly, verbatim: the trial clock is NOT captured by gwbk (its restore behavior is unknown — reset separately via rig trial reset), and tag-provider bulk export is Phase 5 scope (gwbk captures tag config via gateway data). No reader can mistake either for a silent drop.

rig restore --file <path.gwbk> is the guarded inverse (--yes-refusal exit 2 before any discovery, the fifth destructive verb). The POST is a RAW application/octet-stream body — NOT multipart — with the four scope params (restoreDisabled, disableTempProjectBackup, renameEnabled, restoreLocal) sent explicitly as false. Restore is synchronous AND the gateway restarts afterward, so BOTH wire directions ride a 300 s per-request budget and success is a witnessed post-restore StatusPing→RUNNING (the --timeout budget floors at 300 s — a short explicit timeout cannot buy an unknown-state mid-restart report). A bare 2xx never suffices.

⚠ API tokens may be clobbered (Pitfall 5). Tokens stored under CORE config (data/config/CORE/ignition/api-token) are "modified/cleared often by gwbk restores" (83-api) — after a restore, stored profiles may 401. The restore data ALWAYS carries "API tokens may have been reset by restore — re-provision via gateway UI, then ign doctor" as its first warning, in every render mode. 83-api's recommendation: keep durable tokens in an EXTERNAL location, not CORE config.

Both verbs address the rig's derived gateway URL and source their credential from IGNITION_TOKEN (the rig-family chain — the backup route 401s unauthenticated, unlike the trial endpoints; a missing token is exit 3). The round-trip is pinned live by the opt-in e2e gate:

IGNITION_LIVE_URL=http://localhost:9088 \
IGNITION_LIVE_TOKEN='name:key' \
IGNITION_LIVE_MUTATIONS=1 \
cargo test -p ignition-cli --test e2e_rig -- --ignored

(the rig must be discoverable — run from the rig's checkout, set IGNITION_RIG, or configure [rig].default; verify with ign rig status first that the intended rig is the one UP — the gateway verbs address the derived localhost:<port> URL, and a port collision with another stack would silently point them at the WRONG gateway). The gate snapshots a pre-witness project, creates a post-snapshot marker, restores, and asserts BOTH halves: witness SURVIVED, marker GONE.

Gateway backups (ign backup)​

The standalone gwbk surface on ANY profiled gateway — the same wire rig snapshot/rig restore ride (Phase 4), without the rig machinery:

  • ign backup download [-o FILE] [--type roaming|all] — a streamed read (never buffered in memory; 300 s per-request timeout). roaming is the portable backup (cross-gateway — the default); all includes gateway-specific state. Default filename follows the export convention: the gateway's Content-Disposition basename when it sends one, else <profile>-backup.gwbk (streamed to a .part first, renamed on completion — a failed download leaves no half-written impostor).
  • ign backup restore <FILE> --yes — REPLACES this gateway's state from the gwbk. Destructive: the 8th --yes-guarded verb (guard BEFORE resolution — a refusal does zero work). The restore POST is synchronous; the gateway then RESTARTS and blocks for minutes — the 2xx is acceptance, not readiness (ign wait restart is the readiness witness if you want one; rig restore bundles that wait for rigs). API tokens inside the backup may clobber current ones — see the rig restore token warning, same mechanics here.
  • There is NO ign backup list — no native listing endpoint exists (exactly GET/POST on /data/api/v1/backup). Honesty over verb theater: your filesystem is the list. The EAM archive store (storage/archived-backups) is a DIFFERENT thing — it belongs to the EAM controller's fleet backups, not this gateway's gwbk files.

The trial clock is NOT captured by a gwbk (see the rig snapshot exclusions — same mechanics).