jaco apply
Apply a jaco.yaml + compose pair to the cluster. The leader validates
both files, replicates the new Deployment revision through raft, and
the scheduler converges replicas to match.
Synopsis
jaco apply <jaco.yaml> [--compose <path>] [--dry-run]
[--server <host:port> --token <op>]
[--ca-cert <path>] [--socket <path>]Flags
| flag | default | meaning |
|---|---|---|
--server <addr> | — | leader gRPC; omit to use the local socket |
--token <op> | JACO_TOKEN | operator bearer token (with --server) |
--ca-cert <path> | /var/lib/jaco/node/ca.crt | cluster CA PEM |
--socket <path> | /var/run/jaco/jaco.sock | local jacod unix socket |
--compose <path> | auto-detect | path to the compose file |
--dry-run | false | print the diff and exit without applying |
When --compose is unset, the CLI looks for compose.yml then
compose.yaml in the same directory as <jaco.yaml>. If neither
exists, apply fails with no compose file found next to <jaco.yaml>; pass --compose explicitly.
Auth
Operator token (TCP) or unix-socket trust (local).
Behavior
- The CLI reads both files from disk.
- If
jaco.yamldeclares a top-levelenvironment: <path>, the CLI loads the named env file (path resolved relative to the jaco.yaml's directory) and substitutes every${VAR}in the compose document —$VAR,${VAR},${VAR:-default},${VAR:?msg},$$escape. The interpolation source is the env file only; the operator's process environment is NOT consulted. A missing env file fails the apply withload environment file <path>: …; a required${VAR:?msg}reference whose variable is absent fails withinterpolate ${VAR} at line N col M: required variable …. - The CLI folds every service-level
env_file:into the service'senvironment:map (compose-spec precedence: the explicitenvironment:value wins for matching keys; see Supported compose fields →env_fileresolution). - The fully-resolved compose bytes plus the original jaco.yaml
bytes go to the daemon via
Deploy.Apply. The daemon never reads the operator's filesystem.
- If
- The daemon validates the jaco.yaml against the closed schema
(
deployment,services,routes; see manifests/jaco-yaml.md) and the compose file against the supported-field allowlist (see manifests/compose.md). Unknown fields, unknown services, unknown hosts, unknown networks, across-deploymentcollision, attempts to publish a reserved port (80/443), and two services in the same deployment publishing the same host port (port_conflict) reject the apply with a typed error and no state changes. - Cross-check: every
services[*].namein the jaco.yaml must match a key in the compose file. A route targeting a service that setsnetwork_mode: noneornetwork_mode: service:<name>is rejected — those services have no reachable listener of their own, so the route would publish a dead upstream. - Privileged admission (issue #119). A compose service that sets
privileged: trueor a non-emptysecurity_opt:list requires: the calling operator token hasallows_privileged=true(seejaco token issue --allow-privileged), AND the service carrieslabels: { "jaco.io/allow-privileged": "true" }. A missing token flag rejects withPermissionDeniednaming the first offending service; a missing label rejects in step 2 withvalidation_failed. Admitted privileged workloads write oneprivileged_workload_admittedaudit event per gated service. - The leader writes a new
Deployment{applied_revision: N+1}through raft. The scheduler reconcilesReplicaDesiredand the runtime converges containers. - The RPC returns
Applied revision: <N+1>once the leader has committed the new revision. Container start + health is observed asynchronously;jaco status -wshows replicas moving throughpending → pulling → running.
With --dry-run the apply returns the Diff (adds, updates, removes)
without committing. Privileged admission still runs under dry-run
so the diff reflects what the live apply would decide. The diff itself
currently surfaces as No changes on a no-op apply; richer per-entity
diffs are tracked separately.
Exit codes
0— apply succeeded, or--dry-runreturned a diff.1—validation_failed,unknown_service,unknown_host,unknown_network,reserved_port,port_conflict,cannot place N replicas on M pinned hosts,PermissionDenied(privileged admission),quorum_lost,no_leader, or any auth / transport error.
See Status and errors for the closed code set.
Examples
End-to-end apply with the manifest pair side-by-side:
export JACO_TOKEN=<operator_token>
export LEADER=node-1:7000
jaco apply --server $LEADER ./hello/jaco.yaml
# Applied revision: 1Dry-run on the local daemon, using an explicit compose path:
sudo jaco apply --compose ./hello/services.yml --dry-run ./hello/jaco.yaml
# No changesRe-applying with a bumped image rolls one replica at a time:
# edit ./hello/docker-compose.yml: image: nginx:1.28
jaco apply --server $LEADER ./hello/jaco.yaml
jaco status --server $LEADER hello -w # observe the rolloutIf the apply rejects, the cluster state is unchanged. Re-edit and try again — there is no partial-apply state to clean up.