Compose file reference
Full schema reference for the cluster.yaml compose file
The compose file declares the desired state of an Arctic cluster in YAML. Apply it with arctic compose apply. For a task-oriented walkthrough, see the compose guide; for the CLI's own configuration file, see the CLI configuration reference.
File format
Compose files use schema version v1. The file can be named anything, but cluster.yaml is the convention. Two top-level optional blocks, requires and server, were added in v1.4.0; both are backwards-compatible.
Root structure
| Field | Type | Required | Description |
|---|---|---|---|
version | string | Yes | Schema version; must be v1 |
license | string | No | Path to the license file, relative to the config file location |
requires | object | No | Version constraints every peer must satisfy before apply or diff proceeds |
server | object | No | Designated server peer for centralized operations. Absent means fully decentralized |
peers | array | Yes | Peer definitions |
services | array | No | Service definitions |
requires
Version constraints checked after pre-flight connectivity and before diff or apply. Skip with --skip-requires.
| Field | Type | Description |
|---|---|---|
agent | string | Constraint on every peer's running agent version |
Constraint syntax: vX.Y.Z (exact), ~vX.Y.Z (patch range), or ^vX.Y.Z (minor range).
requires:
agent: "^v1.4.0"server
Optional. By default a cluster is managed as a mesh: every peer accepts operator API calls, and the operator needs network access to all peers. Declaring a server block shifts management to a hub-and-spoke shape: the named peer becomes the cluster's point of entry, taking centralized operations such as the cluster apply lock and managing the other peers over the cluster on the operator's behalf.
Only management changes shape. The underlying network is a mesh either way - peers still gossip and carry traffic directly - and the server peer holds no special state authority; "server" is an entry-point role. The default mesh is the simplest and most consistent choice when the operator can reach every peer. Declare a server when the other peers cannot or should not be reached directly, typically together with api_access: internal on those peers so a single front-door node manages an otherwise unreachable cluster. See Clustering for the model.
| Field | Type | Required | Description |
|---|---|---|---|
peer | string | Yes | Name of the primary server peer; must reference a peer in the peers list |
fallback_peer | string | No | Single fallback used only when the primary is unreachable at the connection level |
server:
peer: hub
fallback_peer: hub-standbyPeer configuration
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique identifier for this peer; referenced by services |
description | string | No | Free-form description shown in operator output; not gossiped |
type | string | No | Peer role: agent (default) or server |
api_access | string | No | API exposure: full (default) or internal |
endpoints | array | Yes* | Ordered list of addresses; first responder wins |
address | string | No | Deprecated single endpoint; prefer endpoints |
fingerprint | string | No | Pinned TLS identity fingerprint (SHA256:...), verified before the peer is first contacted |
*At least one endpoint is required: either endpoints (preferred) or the deprecated address. Each endpoint may be a bare host (10.0.0.2), host:port (10.0.0.2:9090), or a full URL (https://node-a.internal).
As of v1.4.2, scheme-less endpoints default to https, and the host's identity is verified on first contact (against the declared fingerprint, or pinned trust-on-first-use). An explicit http:// or https:// scheme is honored exactly as written; a deliberately plaintext cluster must declare explicit http:// endpoints, and apply and diff name any peer that needs it. CLI builds from before the 2026-07-22 v1.4.2 patch instead probed scheme-less endpoints and required explicit https:// on the first bootstrap (see the release notes).
fingerprint pins the peer's Ed25519 TLS identity, and is accepted with or without the SHA256: prefix. When set, compose apply and diff verify the peer presents this fingerprint before the cluster is contacted; a mismatch is always fatal and is never downgraded by --ignore-unreachable. When absent, the CLI falls back to trust-on-first-use pinning. Read the value out-of-band from the trusted host with arctic-agent fingerprint (or from the identity_fingerprint line in the agent's boot journal). See TLS and trust.
An internal peer rejects user-facing endpoints; at least one peer in the cluster must use api_access: full, or the cluster is unmanageable via the API.
Peers cannot be deleted via compose. Removing a peer from the file does not remove it from the cluster. Use arctic peers delete.
Service configuration
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique identifier for this service |
source_peer | string | Yes | Name of the source peer (must exist in peers) |
target_peer | string | Yes | Name of the target peer (must exist in peers) |
transport_type | string | Yes | Tunnel protocol: tcp or kcp |
fully_transparent | bool | No | Fully transparent proxying mode (default false) |
uplink_dev | string | No | Egress device this service's traffic leaves by; empty means the host's auto-detected default |
interface | object | No | MACVLAN interface configuration |
qos | object | No | Quality-of-service settings |
mptcp | object | No | Multipath TCP opt-in for tunnel dials (tcp transport only) |
routes | array | No | Routing rules |
A service needs a routing mode: either interface with enabled: true (MACVLAN interface mode) or one or more routes (policy/CIDR routes mode). source_peer and target_peer must differ. Setting both interface (enabled) and routes on the same service is allowed but triggers an advisory warning, since you typically use one or the other.
The three device fields (uplink_dev, interface.parent_dev, routes[].device) are host-local operator intent and are not gossiped across the cluster. They pin behaviour on multi-NIC hosts, including overlay interfaces such as tailscale0 or a WireGuard device; see the uplinks guide for when to use which.
Interface configuration
| Field | Type | Required | Description |
|---|---|---|---|
enabled | bool | Yes | Create a MACVLAN interface for this service |
vlan_id | integer | No | IEEE 802.1Q VLAN tag; valid range 1-4094 (0 and 4095 reserved). Omit for untagged |
ipv4 | string | No | Desired IPv4 address in CIDR notation (e.g. 10.0.0.1/24). Omit for DHCP |
parent_dev | string | No | Parent device the MACVLAN is built on; empty means auto-detect the egress interface towards the target peer |
mac | string | No | Hardware address mode (see below) |
mac values:
""(omitted): the agent generates a MAC at runtime; it may change when the service is recreated."auto": a deterministic MAC derived from the cluster ID, source peer name, and service name. Stable across applies as long as those names do not change.- explicit MAC: a lowercase colon-separated hex address, e.g.
02:ab:cd:ef:12:34. Must parse cleanly.
ipv4 cannot be 0.0.0.0/0, and IPv4 addresses must be unique per source peer across services.
Omitting ipv4 requests the address over DHCP. This depends on the network
answering DHCP for the interface's (previously unseen) MAC; on networks that
do not, the interface comes up without an IPv4 address and cannot serve as a
route target. Prefer a static ipv4 on the source peer's LAN when in doubt.
QoS configuration
bandwidth_limit_mbps applies to both transports, but differently. On a tcp
service it drives the full traffic shaper (holds the rate, queues flows fairly,
and keeps latency low under load). On a kcp service it acts as a bandwidth
ceiling that caps the link in both directions, without the shaper's fair
queueing or latency control. The remaining fields below (default_rtt_ms,
disable_auto_rtt, memlimit_cap_mb) tune the TCP shaper only and have no
effect on a KCP service.
| Field | Type | Description |
|---|---|---|
bandwidth_limit_mbps | uint64 | Maximum bandwidth in Mbps; 0 means unlimited (no shaping or ceiling) |
default_rtt_ms | uint64 | Seed round-trip time in milliseconds used to tune shaping; 0 uses the system default |
disable_auto_rtt | bool | Disable automatic RTT latency probing for this link |
memlimit_cap_mb | uint64 | Cap the memory the shaper may use, in megabytes; 0 means no cap |
memlimit_cap_mb is useful on memory-constrained hosts to keep the shaper from allocating excessive RAM.
MPTCP configuration
Opts a tcp-transport service's tunnel dials into Multipath TCP, so one tunnel can spread across more than one path at once (it pairs naturally with the device fields above). Only valid with transport_type: tcp; enabling it on a kcp service is a validation error. Both fields are source-peer-local dial settings, like disable_auto_rtt, and are not gossiped.
| Field | Type | Description |
|---|---|---|
enabled | bool | Request MPTCP sockets for this service's tunnel dials |
subflows | uint64 | Requested path-manager subflow count; 0 means the engine default (2), maximum 8 |
Dials fall back to plain TCP when the kernel or the remote peer lacks MPTCP support, and the fallback is silent: if throughput looks single-path, check that both peers' kernels are built with MPTCP and have net.mptcp.enabled on.
Route configuration
| Field | Type | Required | Description |
|---|---|---|---|
source_cidr | string | Conditional | Source network in CIDR notation |
dest_cidr | string | Conditional | Destination network in CIDR notation |
device | string | No | Inbound device this route matches on; empty means the host's auto-detected WAN interface |
priority | uint64 | Yes | Tie-breaker; lower value = higher priority |
At least one of source_cidr or dest_cidr is required. Both cannot be 0.0.0.0/0. Priorities must be unique within a service. device is host-local and not gossiped (see the device-field note under service configuration).
Routes are matched by specificity first, with priority only breaking ties. Specificity order, most specific first: MACVLAN interface match, source plus destination CIDR, source-only CIDR, destination-only CIDR.
Canonical key ordering
arctic compose fmt reorders keys to a canonical order:
| Context | Order |
|---|---|
| Root | version, license, requires, server, peers, services |
| Peer | name, description, type, api_access, address, endpoints, fingerprint |
| Service | name, source_peer, target_peer, transport_type, fully_transparent, uplink_dev, interface, qos, mptcp, routes |
| Interface | enabled, vlan_id, ipv4, parent_dev, mac |
| QoS | bandwidth_limit_mbps, default_rtt_ms, disable_auto_rtt, memlimit_cap_mb |
| MPTCP | enabled, subflows |
| Route | source_cidr, dest_cidr, device, priority |
| Requires | agent |
| Server | peer, fallback_peer |
Validation rules
Validate a file with arctic compose validate cluster.yaml. Errors block apply; warnings are advisory unless you pass --strict.
Errors
| Field | Description |
|---|---|
version | Required; must be v1 |
peers | At least one peer is required |
peers[N].name | Required and unique |
peers[N].endpoints | At least one endpoint required (via endpoints or address) |
peers[N].type | Must be agent or server |
peers[N].api_access | Must be full or internal; at least one peer must be full |
peers[N].fingerprint | Must be a SHA256 fingerprint of a 32-byte digest, optionally prefixed with SHA256: |
server.peer | Required when the server block is present; must reference a peer |
server.fallback_peer | Must differ from server.peer and reference a peer |
services[N].name | Required and unique |
services[N].source_peer | Required; must reference a peer |
services[N].target_peer | Required; must reference a peer; must differ from source |
services[N].transport_type | Required; must be tcp or kcp |
services[N].mptcp.enabled | Requires transport_type: tcp |
services[N].mptcp.subflows | At most 8 (kernel path-manager limit) |
services[N] | Must define interface (enabled) or routes |
services[N].uplink_dev | Valid Linux interface name: at most 15 characters; no whitespace, /, or : |
services[N].interface.ipv4 | Valid CIDR; cannot be 0.0.0.0/0; unique per source peer |
services[N].interface.vlan_id | Must be in range 1-4094 |
services[N].interface.parent_dev | Valid Linux interface name (same rules as uplink_dev) |
services[N].interface.mac | Valid lowercase colon-hex MAC or the literal auto |
services[N].routes[M] | At least one of source_cidr or dest_cidr; not both 0.0.0.0/0 |
services[N].routes[M].device | Valid Linux interface name (same rules as uplink_dev) |
services[N].routes[M].priority | Unique within the service |
Warnings
| Rule | Field | Description |
|---|---|---|
deprecated-address | peers[N].address | address is deprecated; use endpoints |
address-ignored | peers[N].address | Both address and endpoints set; address is ignored |
interface-and-routes | services[N] | Service has both interface and routes; typically use one |
mac-not-locally-administered | services[N].interface.mac | Explicit MAC looks vendor-burned |
mac-multicast | services[N].interface.mac | Explicit MAC has the multicast bit set |
Examples
Minimal configuration
version: v1
peers:
- name: peer-1
endpoints:
- https://192.168.1.10:8080
- name: peer-2
endpoints:
- https://192.168.1.20:8080
services:
- name: tunnel-1-to-2
source_peer: peer-1
target_peer: peer-2
transport_type: tcp
routes:
- dest_cidr: 10.0.0.0/8
priority: 100MACVLAN interface mode
version: v1
license: ./license.json
requires:
agent: "^v1.4.0"
peers:
- name: datacenter-west
description: Primary west DC node
endpoints:
- https://10.0.1.10:8080
- name: datacenter-east
endpoints:
- https://10.0.2.10:8080
- https://10.0.2.11:8080
services:
- name: west-to-east
source_peer: datacenter-west
target_peer: datacenter-east
transport_type: tcp
fully_transparent: true
interface:
enabled: true
ipv4: 10.100.0.1/24
mac: auto
qos:
bandwidth_limit_mbps: 100Policy routes mode with KCP
version: v1
license: ./license.json
peers:
- name: site-a
endpoints:
- https://192.168.1.10:8080
- name: site-b
endpoints:
- https://192.168.2.10:8080
services:
- name: kcp-tunnel
source_peer: site-a
target_peer: site-b
transport_type: kcp
routes:
- source_cidr: 0.0.0.0/0
dest_cidr: 10.20.0.0/16
priority: 100
- dest_cidr: 172.16.0.0/12
priority: 200Device pins and MPTCP
A service pinned to a second WAN interface, with MPTCP enabled so the tunnel can spread across paths. See the uplinks guide for the overlay-interface (tailscale, WireGuard) variants.
version: v1
peers:
- name: peer-a
endpoints:
- https://192.168.1.10:8080
- name: peer-b
endpoints:
- https://192.168.2.10:8080
services:
- name: svc-via-wan2
source_peer: peer-a
target_peer: peer-b
transport_type: tcp
uplink_dev: ens19
mptcp:
enabled: true
subflows: 2
routes:
- dest_cidr: 10.20.0.0/16
priority: 100