Docker/Compose Runtime Contract
This contract defines when Stella Ops may report a Docker or Docker Compose deployment as successful on a non-Kubernetes target.
Success Boundary
A Docker/Compose deployment is successful only when all of these are true:
- The target runtime preflight passed on the target host.
- The selected image reference was resolved to an OCI digest and pulled by the target runtime.
- The workload is running on the target host under the expected Compose project, service, slot, and Stella labels.
- Runtime verification confirms image digest, container/service state, health, and target identity.
stella.version.jsonand deployment evidence record the applied release, target, digest, artifact hashes, and deployment phase results.
Marker files, shell command output, API status, or target connectivity are useful evidence, but they do not satisfy this success boundary by themselves.
Control-Plane Progress Projection
The deployment-monitor compatibility read model is a projection of the typed deployment job; it is not an independent execution state machine. A newly accepted deployment is first persisted as pending. While the library engine runs, the WebApi deployment scope re-reads release_orchestrator.deployment_jobs and, when that state changes, mirrors observed job status plus per-target status/progress into release_orchestrator.deployments. It must not invent a running transition or percentage that the typed job has not persisted. Terminal projection remains a separate step because terminal strategy evidence and target-scoped logs are available only after execution finishes.
Runtime Modes
| Mode | Behavior |
|---|---|
preinstalled | Stella Ops verifies Docker/Compose are already present and new enough. Missing or old runtimes fail before deployment. |
managed | Stella Ops may install or upgrade Docker/Compose from an explicit package source, local mirror, or Offline Kit path. |
detect-only | Stella Ops reports readiness and blockers without changing the target. |
Default version gates for planning:
- Docker Engine:
>= 24.0.0 - Docker Compose plugin:
>= 2.20.0 - Compose command:
docker composev2
Production and offline deployments must not silently install from the public internet. A managed install must record the configured package source type in evidence without leaking credentials.
Preflight Evidence
Preflight runs before image pulls, Compose writes, or deployment hooks with side effects. It records:
- OS and architecture.
- Docker client and server versions.
- Docker daemon status.
- Compose plugin version.
- Registry reachability.
- Requested image platform compatibility.
- Deployment work directory write access.
- Available disk space.
Stable failure codes include:
runtime_missingruntime_daemon_unavailableruntime_version_too_oldcompose_missingregistry_unreachableplatform_unsupportedworkdir_not_writabledisk_space_low
Target Readiness Checks
Target health checks must validate the execution path that Stella Ops will use for the deployment. Ssh and WinRM targets use direct endpoint probes even when an agent id is present, because their tasks execute through the remote transport. Agent-bound DockerHost, ComposeHost, ECS, and Nomad targets must fail closed unless the referenced agent exists, is active, has a fresh heartbeat, and advertises the capabilities required for that target kind.
Database migrations must preserve the health status contract on existing installations. The allowed statuses are unknown, healthy, degraded, unhealthy, offline, and unreachable; legacy constraints must be refreshed in place during startup migration.
Post-Deploy Service Health Verification (as built)
Agent task success proves the deploy commands ran (container started under the compose runtime contract); it does not prove the SERVICE answers. Components opt in to post-deploy verification with healthProbe.* config keys, and the target executor runs the configured probes immediately after the agent deploy task succeeds. A probe failure fails that target’s deployment task with a recorded per-target reason (Post-deploy health probe failed for component '<name>': … in the task error / deployment_json targets[].error), which feeds the strategy gate and the final deployment status like any other target failure.
Supported component config keys (the healthProbe. prefix is distinct from healthcheck., which configures the container’s own docker healthcheck in the compose lock):
| Key | Meaning | Default |
|---|---|---|
healthProbe.type | http or tcp; presence enables the probe | — (opt-in) |
healthProbe.http.url | absolute probe URL (overrides host/port/path/scheme) | — |
healthProbe.http.path | path on the target host | / |
healthProbe.http.port | port | 80 (http) / 443 (https) |
healthProbe.http.scheme | http or https | http |
healthProbe.http.expectedStatus | exact status; unset = any 200–399 | unset |
healthProbe.tcp.port | TCP connect port (required for tcp) | — |
healthProbe.host | probe host override | target connection host |
healthProbe.retries | total attempts (min 1) | 3 |
healthProbe.intervalSeconds | delay between attempts | 5 |
healthProbe.timeoutSeconds | per-attempt timeout | 5 |
healthProbe.initialDelaySeconds | warm-up delay before first attempt | 0 |
Semantics (all fail-closed):
- A misconfigured probe (unsupported type, missing
tcp.port, no resolvable host) fails the deployment task with a stablePost-deploy health probe configuration invalid: …reason. It never silently degrades to “no verification”. - The job-level kill-switch
strategyConfig.waitForHealthCheck=false(DeploymentOptions.WaitForHealthCheck) skips probing but records the skip (healthProbe.status=skipped,healthProbe.reason=waitForHealthCheck=false) so evidence never implies the service was verified. - Probe evidence is written to the task result:
healthProbe.status(passed/failed/skipped),healthProbe.count, and per componenthealthProbe.<name>.endpoint,.healthy,.attempts,.observed. - Components without
healthProbe.*keys behave exactly as before (no probe, no evidence keys).
Probe placement (deliberate, revisit-with-transport-rev): probes execute ORCHESTRATOR-SIDE via IDeploymentHealthProbeRunner (default HttpTcpDeploymentHealthProbeRunner — HTTP GET / TCP connect from the control plane). Agent-side probe tasks are the preferred long-term seam for targets behind firewalls, but the DeploymentAgentTask transport contract currently has no task type that expresses a service probe (compose_preflight is host runtime readiness, not service health), and adding one requires the in-process transport, the mTLS polling transport, and remote Agent.Core binaries to rev in lockstep. The runner interface is transport-agnostic so an agent-side implementation can replace the direct prober without executor changes.
Generated Artifacts
The minimum Compose deployment artifact set is:
compose.stella.lock.ymlstella.version.json- apply manifest with SHA-256 hashes
- optional environment artifact
- previous-revision metadata when a previous successful deployment exists
compose.stella.lock.yml is desired state. It must be deterministic for the same release, target, environment, and slot. Runtime timestamps belong in evidence or stickers, not in the lock file when they would change the desired state hash.
The top-level Compose version value, when present for compatibility with older Compose readers, must be emitted as a YAML string such as version: "3.8". Docker Compose v2.40 rejects an unquoted numeric value with version must be a string.
Agent Task Payload Contract
Container deployment tasks must preserve the full registry/repository image identity and the immutable digest. The runtime bridge must never collapse a component to componentName@digest when the release supplied registry/repository:tag@digest.
External polling assignments deliver the payload string directly to Agent.Core task handlers. Those handlers deserialize with the default System.Text.Json options, so task payload keys must match the handler property names exactly: Image, Name, and PullImage for docker.run; ProjectName, ComposeLock, VersionSticker, PullImages, and BackupExisting for compose.up; Hooks for docker.run and compose.up; ProjectName and optional RevisionPath for compose.rollback; and WorkDirectory, ImageReference, MinimumFreeDiskBytes, RegistryReachabilityMode, and RequireCompose for compose.preflight. DockerHost preflight sets RequireCompose=false by default; ComposeHost sets it to true.
Hook entries are target-side task payload data, not control-plane-only labels. Each entry uses exact property names: Name, Phase, Interpreter, Command, WorkingDirectory, TimeoutSeconds, FailurePolicy, and RedactionPolicy. Supported deployment hook phases are pre-deploy and post-deploy. FailurePolicy=fail is the default and blocks the deployment; ignore, continue, and warn record the hook result without failing the phase. Hook stdout/stderr are redacted by default unless the hook explicitly sets RedactionPolicy=none.
For DockerHost, the agent task carries one digest-pinned image reference, component config, Stella labels, project/slot variables, work directory, and manifest hash. The Docker payload labels must include Stella target identity: stella.target.id, stella.target.name, stella.target.type, stella.target.endpoint, stella.execution.mode, and stella.manifest.hash. Component config keys prefixed with env. are copied into the docker.run payload environment without the prefix, alongside the standard Stella deployment variables. DockerHost payloads request PullImage=true unless the release explicitly marks a preloaded/offline image path with runtime.skipPull, compose.skipPull, compose.pullImages=false, or pullImages=false; when pull is enabled, the agent must pull and verify the digest-pinned image before listing, stopping, or creating containers.
For ComposeHost, the agent task carries:
- full component image references pinned as
image@sha256:... - deterministic
compose.stella.lock.yml stella.version.jsonwith release, job, task, target, slot, project name, work directory, manifest hash, compose hash, and component digests. The Compose agent stages the compose lock first and commits the version sticker only after pull, up, and service inspection complete.- variables for
projectName,slot,targetId,targetName,targetType,targetEndpoint,targetExecutionMode,workDirectory,composePath,versionStickerPath,manifestHash, artifact hashes, deployment job id, deployment strategy, and rollback flag - a target context object in the compose payload with
id,name,type,endpoint, andexecutionMode
The task payload may be delivered to a target-resident agent or to a lab bridge that explicitly executes on the target VM. It must not run Docker/Compose on the orchestrator host while reporting target-side deployment success.
Agent task results must echo the target identity and execution mode that were used for the run. Docker result evidence includes the container id/name/state, IP address, targetId, targetName, targetType, targetEndpoint, targetExecutionMode, manifestHash, completedPhases, phaseDurationsMs, deploymentPhases, hookResults, and failurePhase. Compose result evidence includes the project directory, compose file path, apply manifest path, artifact hashes, service count/list, completedPhases, phaseDurationsMs, deploymentPhases, hookResults, failurePhase, and the same target identity fields. Successful Compose deployment also returns previousRevisionPath when a rollback baseline was retained. A missing Docker/Compose in-process capability is a failure, not permission to run against the orchestrator host.
The target agent exposes compose.rollback for retained revision restoration. It resolves an explicit revision path, or the latest retained revision, restores compose.stella.lock.yml, stella.version.json, and apply-manifest.json under the project lock, validates hashes, pulls rollback images, starts Compose, and records rollbackRestored, restoredRevisionPath, phase timings, and target identity. If no retained revision exists, the task fails with a stable resolve_revision failure phase before any pull or runtime mutation.
Target Layout
The target project directory is owned by Stella Ops. The implementation may vary by OS, but each target must expose these logical paths:
| Logical artifact | Purpose |
|---|---|
| active compose lock | Current desired Compose model |
| version sticker | Current applied release metadata |
| apply manifest | Commit marker and artifact hashes |
| revision store | Previous successful revisions for rollback |
| lock file | Per-project deployment lock |
Compose project names must be stable for the tuple tenant/environment/target/application/slot. They must not be random deployment ids. Valid slots include active, canary, blue, green, and A/B variant ids. If a release supplies an explicit Compose project, Stella Ops appends the non-active slot unless the project name already carries that slot suffix. This prevents a blue, green, canary, or variant deployment from overwriting the active Compose project by reusing the same explicit project name.
Slot identity is deployment evidence. Target-agent Docker and Compose assignments carry slot, trafficRole, and projectName variables. Docker container labels include stella.slot, stella.project, and stella.traffic.role; Compose labels include the same values through the generated lock. Version stickers record both slot and trafficRole. Default slot selection is strategy-aware: rolling/all-at-once use active, canary uses canary, and blue-green/A-B use green unless the release or request strategy config supplies slot, deployment.slot, or variantId.
Deployment Phases
Docker/Compose target execution uses these phases:
preflightpreparepre-deploydeployverifypromotepost-deployevidencecleanup
Target task handlers also expose lower-level implementation phases in completedPhases, such as pull_image, prepare_container, pre_deploy_hooks, write_artifacts, compose_pull, compose_up, inspect_services, write_sticker, and post_deploy_hooks. These names are for diagnostics and tests; the high-level deploymentPhases list remains the stable product evidence surface.
Rollback uses the same executor with previous successful artifacts and records:
rollback-prerollback-deployrollback-verifyrollback-post
Hooks may run in phase-specific points, but hooks are not a substitute for Docker/Compose execution.
Release Script Graph Proof
Release script dependency evidence uses release.script.graph-proof.v1. The proof is generated from explicit script/module nodes and must remain deterministic for the same release graph:
- each node records
moduleName,scriptId,scriptVersion, and phase (startorupdate); - dependency references record the depended-on module name, script id, and optional script version;
startOrder[]andupdateOrder[]are phase-split topological orders, sorted by module name, script id, and version whenever multiple nodes are ready;proofDigestis asha256:digest over schema, status, phase orders, dependency refs, and issues.
The proof fails closed with status=failed when a dependency is missing, a declared dependency version does not match an available script node, duplicate nodes exist, a cycle is detected, or a start-phase script depends on an update-phase script. Update-phase scripts may depend on start-phase scripts; those cross-phase references are preserved in evidence while update ordering is computed from update-to-update dependencies. A failed graph proof is blocking assurance evidence, not a warning.
Strategy Evidence Boundary
Deployment strategy planning is evidence only when the persisted job records the applied strategy, batch indexes, target order, and continuation decision after each batch. Rolling and canary plans must stop further batches when the strategy gate says not to proceed. Unrun tasks after a failed strategy gate must be recorded as Skipped, not left as Pending.
Rollback success requires target-side restoration evidence. A deployment status transition into rolling_back proves the control-plane mutation was accepted, but it is not sufficient evidence that the previous Compose lock, image digest, labels, health, or version sticker were restored.
Automatic rollback on failed deployment (as built)
When a deployment job FAILS — including a post-deploy health-probe failure — the deploy engine (DeployOrchestrator forward path) makes exactly ONE automatic rollback attempt through the same per-target rollback path the operator-initiated pipeline uses (TargetExecutor.RollbackTargetAsync → compose.rollback on the target agent). Behavior:
- Guarded by
strategyConfig.autoRollbackOnFailure(aliasrollbackOnFailure;DeploymentOptions.RollbackOnFailure). DEFAULT ON — fail-closed posture: a failed deploy converges back to the last retained revision unless the operator explicitly opts out. When disabled, the skip is logged and the job staysFaileduntouched. - Eligibility is the shared retained-revision rule (same as the manual pipeline): the task ran and its result carries
projectName,previousRevisionPath,restoredRevisionPath, ortargetType=ComposeHost. If NO target is eligible, no rollback runs and the job staysFailedwith the reason suffixed(auto-rollback skipped: no target retained a previous revision to restore). - Eligible targets are rolled back in reverse batch order. The job transitions
Failed → RollingBack → RolledBackon success; the original failure reason is preserved on the record (suffixed(auto-rollback restored the retained previous revision)). The promotion remainsFailed— rollback is recovery, not deployment success. - If the rollback attempt itself fails, the job ends
Failedwith the combined reason (…; auto-rollback failed: <error>) and is NEVER retried automatically (one attempt: the job is stampedIsRollbackbefore dispatch, and rollback jobs are never auto-rolled-back). - Events:
RollbackStarted/RollbackCompleted/RollbackFailedare published around the attempt.
When the WebApi real deployment engine is enabled, the rollback mutation must dispatch rollback work through the production deployment pipeline, not only update compatibility-store status. The pipeline selects eligible Compose target tasks with retained revision evidence, resets them for rollback, executes rollback batches in reverse batch order, and asks the target executor to publish compose.rollback assignments carrying projectName, target identity, optional rollback.revisionPath, and optional rollback.reason. Targets without Compose revision evidence are skipped with an explicit no-revision reason; if no Compose task is eligible, the rollback fails closed.
Blue-green, canary/gradual, and A/B releases require traffic adapter evidence before Stella Ops may report traffic-shifted success. Without a traffic adapter, Stella Ops may only report standby-slot deployment evidence or fail with a stable no-adapter code; it must not imply traffic moved. Current no-adapter evidence uses strategy.traffic.mode=standby_only, strategy.traffic.status=not_shifted, strategy.traffic.shifted=false, and strategy.traffic.reason=traffic_adapter_missing or traffic_adapter_executor_missing.
If strategyConfig.trafficAdapter resolves to a registered adapter, the WebApi pipeline executes the adapter after target-side deployment evidence exists and before the strategy gate advances. Adapter evidence is projected back to the deployment record under strategyEvidence, including strategy.traffic.mode, strategy.traffic.status, strategy.traffic.shifted, strategy.traffic.controlWeight, strategy.traffic.treatmentWeight, strategy.traffic.slot, and adapter-specific fields. requireTrafficAdapter turns any missing, disabled, or failed adapter into a failed deployment before later batches are started. The opt-in local-file adapter writes route state to disk for repeatable lab verification only; production traffic shifting still requires an actual router/load-balancer adapter.
The opt-in webhook adapter is the generic router-controller boundary. It is disabled unless configured, posts shift and rollback requests to the configured URLs, fails closed on missing URL, timeout, unreachable controller, non-2xx response, or invalid response, and records controller evidence such as adapter kind, webhook URL/status, observed data-plane URL, and applied timestamp. The VM lab proves this boundary with a Playwright-managed Nginx router, but production traffic control still belongs in an operator-owned controller/plugin with the appropriate router credentials and rollback policy.
Traffic rollback uses the same adapter boundary. Once target-side compose.rollback work succeeds, a traffic strategy with adapter configuration must ask the adapter to restore the previous traffic slot and then record strategy.traffic.rollback.mode=traffic_rollback, strategy.traffic.rollback.status, strategy.traffic.rollback.shifted, strategy.traffic.rollback.reason, strategy.traffic.rollback.slot, and any adapter-specific rollback evidence. A required but missing/failed adapter fails the rollback closed with stable evidence such as traffic_adapter_executor_missing or traffic_adapter_rollback_failed.
Completion Evidence
A passing deployment record must include:
- deployment id and target id
- Docker/Compose runtime versions
- Compose project and service names
- image reference and digest
- container id/name and runtime state
- health result
- Stella labels
- version sticker path and hash
- compose lock path and hash
- phase results
- rollback baseline, when present
If rollback succeeds after a failed deployment, the original deployment remains failed. Rollback success is recorded as a separate recovery result.
Rollback evidence generated by the deployment library uses evidenceVersion=release.rollback.evidence.v1. It records proofStatus as complete only when every rollback target has both the current digest and the rollback digest. Missing digest proof is not hidden: each target records hasDigestProof=false and a stable missingProofReason, and the top-level proofStatus becomes incomplete. The proofDigest is a deterministic SHA-256 digest over the rollback proof fields so replay and audit tooling can detect proof drift without depending on the evidence creation timestamp. The WebApi rollback starter persists this payload to release-truth evidence as release.rollback.evidence.v1 when a real rollback pipeline reaches RolledBack. The deployment projection carries rollback.evidence.id, rollback.evidence.hash, rollback.evidence.proofDigest, rollback.evidence.proofStatus, rollback.evidence.orderProofDigest, and rollback.evidence.orderProofStatus in strategy evidence so UI, audit, and replay tooling can navigate from the rollback result to the stored evidence packet.
The same rollback evidence also records order proof for multi-module releases. deploymentOrder[] captures the original failed job’s task/module order, including module name, target identity, batch index, timestamps, status, dependency names, failure reason, and release-script proof digest when carried by the task result. rollbackOrder[] captures the terminal rollback job’s task/module order using the same shape. failure identifies the failed module that triggered rollback. orderProofStatus=complete requires non-empty deployment and rollback order plus a failed module; otherwise it is incomplete. orderProofDigest is a deterministic SHA-256 digest over these order/failure fields. This is a replay/audit proof, not a substitute for the live target-side rollback evidence required above.
