Tillered Docs

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

ScenarioRecommended Approach
Initial cluster setupcompose apply
Managing multiple environmentscompose apply
Production deploymentscompose apply
CI/CD deploymentscompose apply
Quick one-off changesImperative commands
Debugging/explorationImperative commands

Basic Workflow

The typical workflow for using compose is:

  1. Init a starter configuration (for new clusters)
  2. Export existing configuration (if upgrading from imperative management)
  3. Edit the YAML file to define desired state
  4. Format the configuration for consistency
  5. Validate the configuration
  6. Diff to preview changes
  7. 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.yaml

Configuration 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: 100

A 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: 100

Top-level Fields

FieldRequiredDescription
versionyesSchema version. Currently v1.
licensenoRelative path to the license file (resolved from the config file location). Used for bootstrap when all peers are unbootstrapped.
requiresnoVersion constraints every peer must satisfy. See below.
servernoDesignated server peer for centralized operations. Absent means fully decentralized. See below.
peersyesPeers (nodes) in the cluster.
servicesyesServices (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"
SyntaxMeaning
vX.Y.ZExact version.
~vX.Y.ZPatch range (any vX.Y.* at or above the given patch).
^vX.Y.ZMinor 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
FieldRequiredDescription
peeryesName of the primary server peer. Must reference a peer in peers.
fallback_peernoSingle 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.

FieldRequiredDescription
nameyesUnique human-readable identifier.
descriptionnoFree-form description shown in operator output. Not gossiped, no routing effect.
typenoPeer role: agent (default) or server.
api_accessnoAPI exposure: full (default) or internal. Internal-only peers reject user-facing endpoints and are reachable via the agent's recovery token only.
endpointsyesOrdered 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).
fingerprintnoExpected 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.

FieldRequiredDescription
nameyesUnique human-readable identifier.
source_peeryesPeer name where the service originates.
target_peeryesPeer name where the service terminates.
transport_typeyesTunnel protocol: tcp or kcp.
fully_transparentnoEnables fully transparent proxying mode.
uplink_devnoEgress 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.
interfacenoMACVLAN interface configuration. See below.
qosnoQuality of service settings. See below.
mptcpnoMultipath TCP dial settings (TCP transport only). See below.
routesnoPolicy / 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.

FieldRequiredDescription
enabledyesEnables MACVLAN interface creation for this service.
vlan_idnoIEEE 802.1Q VLAN tag, 1-4094. Omit (or 0) for no VLAN tagging.
ipv4noDesired IPv4 address in CIDR notation. Omit for DHCP.
parent_devnoParent device the MACVLAN is built on. Empty auto-detects the egress interface towards the target peer. Host-local; not gossiped.
macnoHardware 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.

FieldRequiredDescription
bandwidth_limit_mbpsnoMaximum bandwidth in Mbps. 0 disables shaping and RTT probing for this link.
default_rtt_msnoSeed round-trip time in milliseconds used to tune shaping (0 uses the system default).
disable_auto_rttnoDisables RTT latency probing for this link when true.
memlimit_cap_mbnoCaps 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.

FieldRequiredDescription
enabledyesWhen true, tunnel dials request an MPTCP socket.
subflowsnoRequested path-manager subflow count. 0 means the engine default of 2; maximum 8.

routes

Canonical field order: source_cidr, dest_cidr, device, priority.

FieldRequiredDescription
source_cidrnoSource network in CIDR notation.
dest_cidrnoDestination network in CIDR notation.
devicenoInbound interface this route matches traffic on. Empty matches on the host's auto-detected WAN interface. Host-local; not gossiped.
priorityyesTie-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.yaml

Validation 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.yaml

The 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.yaml

This will:

  1. Run pre-validation (lint)
  2. Check pre-flight connectivity to each peer
  3. Enforce any requires constraints
  4. Show the full planned changes, including any bootstrap and pending peer joins
  5. Prompt for confirmation
  6. 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

FlagDefaultDescription
--dry-runfalseShow changes without applying.
--prunefalseDelete resources not present in the config.
--ignore-unreachablefalseSkip peers that cannot be contacted.
--license-file <path>-License file used for bootstrap.
--credentials-file <path>break-glass.<cluster-id>.jsonWhere 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-configtrueSave the cluster to the CLI config and set it as current. Use --save-config=false to disable.
--skip-validatefalseSkip pre-validation checks.
--strictfalseTreat validation warnings as errors.
--no-lockfalseSkip the cluster-wide apply lock (the local lock still applies).
--skip-preflightfalseSkip the pre-apply reachability check of peers.
--skip-requiresfalseSkip the requires.agent version check.
--state-dir <path>.arcticDirectory for the local state cache, lock, and backups. Defaults to .arctic/ next to the config file.
--backup-retention <n>5Number of rollback snapshots to keep (1-100).

Dry Run

Preview changes without applying:

arctic compose apply cluster.yaml --dry-run

Prune Orphaned Resources

Delete services not defined in the configuration:

arctic compose apply cluster.yaml --prune

Without --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-unreachable

Bootstrap 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.json

A 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.yaml

This 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 committed

Commit 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.yaml

Credential Management

The break-glass credentials file (break-glass.<cluster-id>.json by default) and any --env-file output are secrets:

  1. Never commit credentials to version control
  2. 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 list

Fix: Ensure all peer references match peer names defined in the peers section.

Connection Errors

Error: failed to connect to peer agent-2: connection refused

Options:

  • Verify the peer endpoints are correct
  • Check network connectivity
  • Use --ignore-unreachable to 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.yaml

See Also

On this page