Compose
Manage your Arctic cluster using YAML configuration files
This guide explains how to use the arctic compose command to manage your cluster declaratively using YAML configuration files.
Configuration Builder
Use the Configuration Builder to visually define peers and services, then export a ready-to-use cluster.yaml.
Overview
The compose command is the recommended way to deploy and manage Arctic clusters. It provides an Infrastructure as Code (IaC) approach where you define your desired cluster state in a YAML file and apply it declaratively.
Benefits
- Version Control: Track cluster configuration changes in Git
- Reproducibility: Deploy identical configurations across environments
- Review Process: Use pull requests to review changes before applying
- Automation: Integrate with CI/CD pipelines
When to Use Compose vs Imperative Commands
| Scenario | Recommended Approach |
|---|---|
| Initial cluster setup | compose apply |
| Managing multiple environments | compose apply |
| Production deployments | compose apply |
| CI/CD deployments | compose apply |
| Quick one-off changes | Imperative commands |
| Debugging/exploration | Imperative commands |
Basic Workflow
The typical workflow for using compose is:
- Init a starter configuration (for new clusters)
- Export existing configuration (if upgrading from imperative management)
- Edit the YAML file to define desired state
- Format the configuration for consistency
- Validate the configuration
- Diff to preview changes
- Apply to make changes
# Create starter config (new clusters)
arctic compose init --file cluster.yaml
# Or export existing state (existing clusters)
arctic compose export --file cluster.yaml
# Edit cluster.yaml as needed
# Format after editing (optional but recommended)
arctic compose fmt cluster.yaml --write
# Validate the config
arctic compose validate cluster.yaml
# Preview changes
arctic compose diff cluster.yaml
# Apply changes
arctic compose apply cluster.yamlConfiguration File Structure
The schema version is v1. The top-level keys, in canonical order, are
version, license, requires, server, peers, and services.
Minimal Example
version: v1
peers:
- name: agent-1
endpoints:
- https://192.168.1.10:8080
- name: agent-2
endpoints:
- https://192.168.1.20:8080
services:
- name: tunnel-1-to-2
source_peer: agent-1
target_peer: agent-2
transport_type: tcp
routes:
- source_cidr: 0.0.0.0/0
dest_cidr: 10.0.0.0/8
priority: 100A service needs transport_type and either an interface block or routes to
route traffic.
Peers cannot be deleted via compose
Removing a peer from the configuration file does not remove it from the
cluster. Use arctic peers delete (or the API) to remove peers.
Full Example
version: v1
license: ./license.json
requires:
agent: "^v1.4.0"
server:
peer: datacenter-west
fallback_peer: datacenter-east
peers:
- name: datacenter-west
description: Primary west DC agent
type: agent
api_access: full
endpoints:
- https://west.internal:8080
- https://10.0.1.10:8080
- name: datacenter-east
type: agent
endpoints:
- https://10.0.2.10:8080
services:
- name: west-to-east
source_peer: datacenter-west
target_peer: datacenter-east
transport_type: tcp
fully_transparent: true
interface:
enabled: true
vlan_id: 100
ipv4: 10.100.0.1/24
mac: auto
qos:
bandwidth_limit_mbps: 100
default_rtt_ms: 30
disable_auto_rtt: false
memlimit_cap_mb: 64
routes:
- source_cidr: 0.0.0.0/0
dest_cidr: 10.0.2.0/24
priority: 100
- name: east-to-west
source_peer: datacenter-east
target_peer: datacenter-west
transport_type: tcp
fully_transparent: true
routes:
- source_cidr: 0.0.0.0/0
dest_cidr: 10.0.1.0/24
priority: 100Top-level Fields
| Field | Required | Description |
|---|---|---|
version | yes | Schema version. Currently v1. |
license | no | Relative path to the license file (resolved from the config file location). Used for bootstrap when all peers are unbootstrapped. |
requires | no | Version constraints every peer must satisfy. See below. |
server | no | Designated server peer for centralized operations. Absent means fully decentralized. See below. |
peers | yes | Peers (nodes) in the cluster. |
services | yes | Services (tunnels) between peers. |
requires
Declares version constraints that every peer in the cluster must satisfy. The constraint is checked after pre-flight connectivity and before diff or apply.
requires:
agent: "^v1.4.0"| Syntax | Meaning |
|---|---|
vX.Y.Z | Exact version. |
~vX.Y.Z | Patch range (any vX.Y.* at or above the given patch). |
^vX.Y.Z | Minor range (any vX.* at or above the given minor). |
server
When present, apply targets this peer (with an optional single fallback) for all authenticated requests. Omitting the block keeps Arctic in fully decentralized mode.
server:
peer: datacenter-west
fallback_peer: datacenter-east| Field | Required | Description |
|---|---|---|
peer | yes | Name of the primary server peer. Must reference a peer in peers. |
fallback_peer | no | Single fallback used when the primary is unreachable at connection level. This is a recovery path, not a load balancer; lock state held by the primary is not visible from the fallback. |
peers
Each peer is a node in the cluster. Canonical field order: name,
description, type, api_access, address (deprecated), endpoints,
fingerprint.
| Field | Required | Description |
|---|---|---|
name | yes | Unique human-readable identifier. |
description | no | Free-form description shown in operator output. Not gossiped, no routing effect. |
type | no | Peer role: agent (default) or server. |
api_access | no | API exposure: full (default) or internal. Internal-only peers reject user-facing endpoints and are reachable via the agent's recovery token only. |
endpoints | yes | Ordered list of addresses. The CLI tries them in order; the first to respond wins. Each entry may be a bare host (10.0.0.2), host:port (10.0.0.2:9090), or a full URL (https://node-a.internal). |
fingerprint | no | Expected TLS identity of the agent, as SHA256:.... When declared, apply and diff verify the agent presents this identity before first contact, and a mismatch aborts the run. Peers without one fall back to trust-on-first-use. See TLS and trust. |
address is deprecated
The legacy single-value address field still parses, but new configs should
use endpoints. If both are set, address is ignored and the linter emits a
deprecation warning.
services
Each service is a tunnel between two peers. Canonical field order: name,
source_peer, target_peer, transport_type, fully_transparent,
uplink_dev, interface, qos, mptcp, routes.
| Field | Required | Description |
|---|---|---|
name | yes | Unique human-readable identifier. |
source_peer | yes | Peer name where the service originates. |
target_peer | yes | Peer name where the service terminates. |
transport_type | yes | Tunnel protocol: tcp or kcp. |
fully_transparent | no | Enables fully transparent proxying mode. |
uplink_dev | no | Egress device the service's tunnel traffic leaves by (for example ens19 or tailscale0). Empty keeps the host's auto-detected default. Host-local; not gossiped. |
interface | no | MACVLAN interface configuration. See below. |
qos | no | Quality of service settings. See below. |
mptcp | no | Multipath TCP dial settings (TCP transport only). See below. |
routes | no | Policy / CIDR routing rules. See below. |
A service uses one of two routing modes: a MACVLAN interface
(interface.enabled: true) or policy/CIDR routes.
The uplink_dev, interface.parent_dev, and routes[].device fields select
devices on multi-NIC and overlay hosts; see
Uplinks and device selection for when to
use which, and the
compose file reference for the full
schema.
interface
Canonical field order: enabled, vlan_id, ipv4, parent_dev, mac.
| Field | Required | Description |
|---|---|---|
enabled | yes | Enables MACVLAN interface creation for this service. |
vlan_id | no | IEEE 802.1Q VLAN tag, 1-4094. Omit (or 0) for no VLAN tagging. |
ipv4 | no | Desired IPv4 address in CIDR notation. Omit for DHCP. |
parent_dev | no | Parent device the MACVLAN is built on. Empty auto-detects the egress interface towards the target peer. Host-local; not gossiped. |
mac | no | Hardware address. "" lets the agent pick at runtime (may change across recreations); auto derives a deterministic MAC from cluster ID, source peer, and service name; or an explicit colon-separated lowercase hex MAC (aa:bb:cc:dd:ee:ff). |
qos
Canonical field order: bandwidth_limit_mbps, default_rtt_ms,
disable_auto_rtt, memlimit_cap_mb.
| Field | Required | Description |
|---|---|---|
bandwidth_limit_mbps | no | Maximum bandwidth in Mbps. 0 disables shaping and RTT probing for this link. |
default_rtt_ms | no | Seed round-trip time in milliseconds used to tune shaping (0 uses the system default). |
disable_auto_rtt | no | Disables RTT latency probing for this link when true. |
memlimit_cap_mb | no | Caps the calculated shaper memlimit in megabytes (0 disables the cap). Use on memory-constrained hosts. |
mptcp
Canonical field order: enabled, subflows. TCP transport only; declaring
mptcp on a KCP service is rejected. Both fields are source-peer-local dial
settings and are not gossiped. The kernel on both peers must support MPTCP,
and the fallback to plain TCP is silent; see
Multipath TCP.
| Field | Required | Description |
|---|---|---|
enabled | yes | When true, tunnel dials request an MPTCP socket. |
subflows | no | Requested path-manager subflow count. 0 means the engine default of 2; maximum 8. |
routes
Canonical field order: source_cidr, dest_cidr, device, priority.
| Field | Required | Description |
|---|---|---|
source_cidr | no | Source network in CIDR notation. |
dest_cidr | no | Destination network in CIDR notation. |
device | no | Inbound interface this route matches traffic on. Empty matches on the host's auto-detected WAN interface. Host-local; not gossiped. |
priority | yes | Tie-breaker order. A lower value is higher priority. |
Routes are evaluated by specificity first (MACVLAN interface match > source+dest
CIDR > source CIDR > dest CIDR), and priority only breaks ties between routes
of equal specificity.
Validating Configuration
Always validate your configuration before applying:
arctic compose validate cluster.yamlValidation runs parse, then schema validation, then lint. Lint warnings pass
by default; pass --strict to treat warnings as errors. The command supports
--json for machine-readable output and --quiet to show errors only.
Previewing Changes
Use diff to see what will change before applying:
arctic compose diff cluster.yamlThe diff legend is:
+create-delete (shown with--prune)~modify
On a fresh working directory (no .arctic/ state yet), diff prints a
one-line notice that drift detection is skipped and writes nothing. The state
directory is created by apply, not diff.
Applying Configuration
Basic Apply
arctic compose apply cluster.yamlThis will:
- Run pre-validation (lint)
- Check pre-flight connectivity to each peer
- Enforce any
requiresconstraints - Show the full planned changes, including any bootstrap and pending peer joins
- Prompt for confirmation
- Apply changes, with peer joins running inside the apply lock
Changed in v1.4.2
The plan and confirmation now run before any mutation. Earlier versions
joined peers (and, on a fresh cluster, ran the bootstrap itself) before the
prompt, so a run without --yes could change the cluster and then abort. As
of v1.4.2 a fresh cluster shows a complete plan synthesized from the config
before a credential is even minted, joins happen only after approval, and
--dry-run is safe on a fresh cluster. Relatedly, a no-op apply no longer
re-seeds peer endpoint lists; run arctic cluster sync when you want to
force a re-kick.
Apply Flags
| Flag | Default | Description |
|---|---|---|
--dry-run | false | Show changes without applying. |
--prune | false | Delete resources not present in the config. |
--ignore-unreachable | false | Skip peers that cannot be contacted. |
--license-file <path> | - | License file used for bootstrap. |
--credentials-file <path> | break-glass.<cluster-id>.json | Where the bootstrap (break-glass) credentials are written, next to the CLI config by default. The literal value none prints them once without writing a file. |
--env-file <path> | - | Write bootstrap credentials to a .env file. |
--save-config | true | Save the cluster to the CLI config and set it as current. Use --save-config=false to disable. |
--skip-validate | false | Skip pre-validation checks. |
--strict | false | Treat validation warnings as errors. |
--no-lock | false | Skip the cluster-wide apply lock (the local lock still applies). |
--skip-preflight | false | Skip the pre-apply reachability check of peers. |
--skip-requires | false | Skip the requires.agent version check. |
--state-dir <path> | .arctic | Directory for the local state cache, lock, and backups. Defaults to .arctic/ next to the config file. |
--backup-retention <n> | 5 | Number of rollback snapshots to keep (1-100). |
Dry Run
Preview changes without applying:
arctic compose apply cluster.yaml --dry-runPrune Orphaned Resources
Delete services not defined in the configuration:
arctic compose apply cluster.yaml --pruneWithout --prune, apply never deletes anything, but it does list services
that exist in the cluster and not in the config, and reports them in the JSON
result as a prunable array. The listing is informational; --prune remains
the only opt-in for deletion.
Use --prune with caution. Review the plan with --dry-run first. Note that
pruning never deletes peers.
Handle Unreachable Peers
Skip peers that cannot be contacted:
arctic compose apply cluster.yaml --ignore-unreachableBootstrap with License
When all peers are unbootstrapped, the first peer is bootstrapped using the
license. Provide the license via --license-file or the top-level license
key:
arctic compose apply cluster.yaml --license-file license.jsonA bootstrap run leaves you with two credentials. A scoped working credential
is saved into the CLI config and used automatically for day-to-day commands.
The admin (break-glass) pair is printed once and also written to
break-glass.<cluster-id>.json (mode 0600) next to the CLI config; move that
file to secure offline storage and delete it from the machine. Use
--credentials-file to pick a different destination, or the literal value
none for print-only. See the
quickstart for the bootstrap
walkthrough and
credential management
for which credential each operation needs.
Rolling Back an Apply
Every non-bootstrap apply captures a snapshot pair into .arctic/backup/ just
before executing its plan: a copy of the pre-apply state cache plus a config
export of the pre-apply cluster, stored under one timestamp. Capture happens
before execution on purpose, because a half-applied cluster is exactly when
the snapshot is needed. A no-op apply has nothing to execute and captures
nothing.
# List the available snapshots
arctic state backups
# Preview a rollback to the newest snapshot
arctic compose rollback --dry-run
# Roll back to the newest snapshot
arctic compose rollback
# Roll back to a specific snapshot from the listing
arctic compose rollback <timestamp>compose rollback re-applies the snapshot through the standard apply
pipeline with pruning enabled, so the cluster converges back to its pre-apply
configuration. It behaves like any other apply: plan preview, confirmation
prompt, --dry-run, locks, and its own pre-rollback snapshot so a rollback
can itself be undone. Peers are never pruned, so rollback's blast radius is
services and routes.
Retention counts snapshots and is controlled by --backup-retention or
preferences.backup_retention in the
CLI config. Backups taken by
versions before v1.4.2 were state files only; they still appear in
arctic state backups but are listed as non-rollbackable.
See Backup and restore for how rollback fits into the wider recovery story.
Exporting Configuration
Export current cluster state to YAML:
# To stdout
arctic compose export
# To file
arctic compose export --file cluster.yamlThis is useful for:
- Migrating from imperative to declarative management
- Creating backups
- Starting a new configuration from existing state
The license field is left blank on export; fill it in manually before
applying to a fresh cluster.
The Project Directory
cluster.yaml is an operator-side file. It lives with you, on any machine
that can reach the peers (a workstation is typical), not on an agent host and
not under /opt/tillered. Applying it needs no root access anywhere.
The recommended layout is a small git-managed project directory per cluster:
test-cluster/
|-- .git/
|-- cluster.yaml # committed
|-- license.json # committed
`-- .arctic/ # local state cache - not committedCommit cluster.yaml and license.json. The .arctic/ directory is the
CLI's local state cache (state, apply lock, and rollback snapshots): it holds
no secrets, is safe to delete and regenerate, and apply writes a catch-all
.gitignore inside it so it cannot be committed by accident.
Changed in v1.4.2
.arctic/ now anchors to the directory containing the config file, not the
directory you run the command from, so the same cluster.yaml applied from
two places uses one cache. A legacy cwd-anchored directory is moved to the
new location on the next apply with a one-time notice; this fallback is
removed in v1.4.3. --state-dir still overrides the location verbatim.
Project-Scope Target Resolution
Commands run from the project directory resolve their target from
cluster.yaml automatically: arctic peers list, arctic services get,
arctic cluster status, and the rest need no --url or saved context when a
cluster.yaml is present. Point at a different file with --project-file or
ARCTIC_PROJECT_FILE; the literal value none disables project resolution.
Precedence is: explicit flags beat the project file, and the project file
beats the ambient current-cluster context.
This also makes CI simple: a checked-out cluster.yaml plus
ARCTIC_CLIENT_ID/ARCTIC_CLIENT_SECRET (or the matching flags) is a
complete setup, with no CLI config home needed at all.
Best Practices
Use Version Control
Store your configuration files in Git (see the project directory above for the recommended layout):
git add cluster.yaml license.json
git commit -m "Add west-to-east tunnel service"Review Before Applying
Always use diff and --dry-run before applying to production:
# First, see the diff
arctic compose diff production.yaml
# Then, dry run
arctic compose apply production.yaml --dry-run
# Finally, apply
arctic compose apply production.yamlCredential Management
The break-glass credentials file (break-glass.<cluster-id>.json by default)
and any --env-file output are secrets:
- Never commit credentials to version control
- Move the break-glass file to secure offline storage (e.g. a secrets manager) and delete it from the machine once secured
Troubleshooting
Validation Errors
ERROR - Configuration invalid
Errors:
- services[0]: source_peer "unknown" not found in peers listFix: Ensure all peer references match peer names defined in the peers
section.
Connection Errors
Error: failed to connect to peer agent-2: connection refusedOptions:
- Verify the peer endpoints are correct
- Check network connectivity
- Use
--ignore-unreachableto skip
Drift Detection
If the cluster state has drifted from your configuration:
# See differences
arctic compose diff cluster.yaml
# Re-apply to restore desired state
arctic compose apply cluster.yamlSee Also
- CLI Reference - CLI command reference
- Configuration Reference - YAML schema reference
- Uplinks and device selection - the
uplink_dev,parent_dev,device, andmptcpfields in depth - TLS and trust - the
fingerprint:field and trust-on-first-use - Backup and restore - rollback snapshots in the wider recovery story