Tillered Docs

Quickstart

Create a cluster using compose apply

By the end of this quickstart you will have two hosts accelerating TCP between them. You define a small cluster (two peers and one service) in a YAML compose file, apply it with a single command, and route your traffic through it.

Set Up a Project Directory

cluster.yaml is an operator-side file. It stays with you, on your workstation or any machine that can reach the peers; it does not live on the agent hosts, does not go in /opt/tillered, and needs no root. A small git-managed project directory works well:

mkdir my-cluster && cd my-cluster
git init
cp /path/to/license.json .

Commit cluster.yaml (created below) and license.json. The first apply also creates a .arctic/ directory next to the file: a generated local state cache. It holds no secrets and is safe to delete, and apply writes a .gitignore inside it so it cannot be committed by accident; leave it out of version control.

Working from this directory has a second benefit: the CLI resolves its target cluster from the cluster.yaml in the current directory, so commands run here need no extra setup. More on that in Verify the Setup.

Create a Cluster Configuration

Configuration Builder

Prefer a visual tool? Use the Configuration Builder to build and validate your cluster.yaml through a web interface. It runs the same validation rules as the CLI.

Create a file called cluster.yaml with your cluster configuration:

version: v1

license: license.json    # Path to your license file

peers:                   # Define all hosts in the cluster
  - name: node-a
    endpoints:
      - https://192.168.1.10:8080
  - name: node-b
    endpoints:
      - https://192.168.1.20:8080

services:
  - name: tunnel-a-to-b
    source_peer: node-a  # Where traffic enters
    target_peer: node-b  # Where traffic exits
    transport_type: tcp
    interface:
      enabled: true          # Create a MACVLAN interface for routing
      ipv4: 192.168.1.50/24  # Static address on the LAN, used as the route target

Each peer can list more than one entry under endpoints (for example a LAN address and a public address); the cluster tries them in order. Write endpoints with an explicit https:// scheme: it makes the intent unambiguous, and v1.4.2 CLI builds from before the 2026-07-22 patch fail the very first bootstrap with a connection-reset error without it (see TLS and trust). The full schema is in the compose reference.

Pick an unused address on the source peer's LAN for interface.ipv4; clients reach the tunnel by routing through it. Omitting ipv4 requests a DHCP lease instead, but many networks do not answer DHCP for a MAC they have never seen, and the interface then sits without an address and routes nothing - a static address is the predictable choice for a first cluster.

Place your license.json file in the same directory as cluster.yaml.

Apply the Configuration

Apply the configuration to create your cluster:

arctic compose apply ./cluster.yaml

Apply shows the full plan and prompts before it touches anything. On a fresh cluster the plan covers everything: the bootstrap itself, each peer join, and each service create. Nothing mutates until you confirm. Pass --yes to skip the prompt in scripts, or --dry-run to print the plan and exit without changing anything, which is safe even on a fresh cluster. Along the way, apply learns and pins each agent's TLS identity on first contact; see TLS and trust.

Running pre-flight connectivity checks...
  [ok] node-a
  [ok] node-b
Discovering peers...
  [new] node-a (192.168.1.10:8080)
  [new] node-b (192.168.1.20:8080)

Planned changes:
  + peer: bootstrap "node-a" (192.168.1.10:8080)
  + peer: join "node-b" (192.168.1.20:8080)
  + service: create "tunnel-a-to-b" (node-a -> node-b)

Apply these changes? [y/N]:

In a script or CI pipeline there is no terminal to prompt on; a non-TTY run without --yes prints the plan and then exits with code 2 rather than mutating anything.

Bootstrap Credentials

On success, the tail of a first apply looks like this:

Cluster bootstrapped successfully!

Save these credentials (cannot be retrieved later):
  export ARCTIC_CLIENT_ID=cli_01HXYZABC123...
  export ARCTIC_CLIENT_SECRET=sec_abc123...

Break-glass credentials written to ~/.config/arctic/break-glass.clu_01HABC....json (0600).
Store the pair somewhere safe (offline) and delete the file once secured;
pass --credentials-file none to keep it off disk entirely.
...
Configuration applied successfully.
Working credential minted for CLI use (scoped, no admin).
Cluster saved to config (working credential) and set as current

A first apply mints two credentials for the new cluster:

  • A scoped working credential, saved to ~/.config/arctic/config.yaml. All day-to-day commands (peers list, services get, later compose apply runs) use it automatically; you do not need to export anything.
  • The break-glass admin pair, printed once and also written to break-glass.<cluster-id>.json (mode 0600) next to the CLI config. You need it again only for admin-only operations such as credential management, supplied per invocation via ARCTIC_CLIENT_ID/ARCTIC_CLIENT_SECRET or the --client-id/--client-secret flags.

Move the break-glass pair somewhere offline (a password manager or vault) and delete the file once it is secured. --credentials-file picks a different destination for it; the literal value none restores print-only behaviour.

Changed in v1.4.2

Earlier releases saved a single admin credential to the CLI config and printed it exactly once. As of v1.4.2 the config holds a least-privilege working credential instead, and the admin pair is break-glass only, written to break-glass.<cluster-id>.json so scripted runs cannot lose it. If you lose both credentials, see Access recovery.

Verify the Setup

After compose apply returns, confirm the cluster came up as expected. Run these from the project directory: the CLI resolves the target cluster from cluster.yaml and uses the saved working credential, so no --url flag or context setup is needed.

# List peers and their connectivity
arctic peers list

# List services
arctic services list

# Get full service details (including the assigned interface IP)
arctic services get <service-id>

The <service-id> is a ULID shown in the output of arctic services list. peers list and services list give you a summary table; services get returns the full detail for one service.

Updating Your Cluster

cluster.yaml is the source of truth. To change the cluster, edit the file and re-run the same apply flow. Validate first, preview the diff, then apply:

# Check the configuration is well-formed
arctic compose validate ./cluster.yaml

# Preview what would change against the live cluster
arctic compose diff ./cluster.yaml

# Apply the changes
arctic compose apply ./cluster.yaml

The CLI compares your file against the live cluster and updates only what changed, so re-applying an unchanged file is a no-op.

Service Configuration Options

The quickstart example uses the default MACVLAN interface mode. Here are additional configuration options.

Static IP Assignment

Assign a static IPv4 address to the service interface:

services:
  - name: tunnel-a-to-b
    source_peer: node-a
    target_peer: node-b
    transport_type: tcp
    interface:
      enabled: true
      ipv4: 192.168.3.32/24  # Static IPv4 with subnet

Policy-Based Routing

Route specific subnets through the tunnel instead of using a MACVLAN interface. Use this when MACVLAN is unavailable (for example, some cloud environments):

services:
  - name: tunnel-a-to-b
    source_peer: node-a
    target_peer: node-b
    transport_type: tcp
    routes:
      - dest_cidr: 172.31.8.0/24
        priority: 100
      - dest_cidr: 172.31.0.0/16
        priority: 200

Priority breaks ties between routes of equal specificity, and a lower priority value wins. In the example above, traffic to 172.31.8.0/24 matches the first route (priority 100) before the broader 172.31.0.0/16 route (priority 200). Specificity is checked before priority: a MACVLAN interface match beats a source+dest CIDR match, which beats a source-only CIDR, which beats a dest-only CIDR.

Two-Way Communication

A service carries traffic one way, from its source peer to its target peer. To accelerate traffic in both directions between the same two hosts, define a second service pointing the other way. Each direction is its own service with its own interface:

version: v1

license: license.json

peers:
  - name: node-a
    endpoints:
      - https://192.168.1.10:8080
  - name: node-b
    endpoints:
      - https://192.168.1.20:8080

services:
  - name: tunnel-a-to-b
    source_peer: node-a
    target_peer: node-b
    transport_type: tcp
    interface:
      enabled: true
  - name: tunnel-b-to-a
    source_peer: node-b
    target_peer: node-a
    transport_type: tcp
    interface:
      enabled: true

Traffic entering node-a exits at node-b through tunnel-a-to-b, and traffic entering node-b exits at node-a through tunnel-b-to-a. The Configuration Builder can build and validate multi-service configurations like this, with the same rules the CLI applies.

Troubleshooting

Agent Not Starting

Check logs for errors:

journalctl -u arctic -f

Common causes: port 8080 already in use, or the agent not running as root.

Compose Apply Fails

  • Verify all agents are running: curl -sk https://<ip>:8080/livez (the -k is expected; the agent's certificate is self-signed, see Installation)
  • Check the license file exists and is valid (the agent re-verifies the license on every startup)
  • Ensure network connectivity between agents

Handshake Fails

  • Both agents must use the same license
  • Agents must reach each other on port 8080
  • Check for firewall rules blocking ports 8080/TCP, 51840/UDP, and 61000/TCP

More failure modes, including TLS and credential errors, are covered in Troubleshooting.

Next Steps

You now have a working Arctic cluster with encrypted tunnels between your hosts. Explore the Compose guide for more advanced cluster management options.

On this page