Tillered Docs

Uplinks and device selection

Route a service over a second WAN NIC, tailscale, wireguard, or any local VPN interface

Arctic normally auto-detects one egress interface per host and sends every service through it. That is the right default for a single-NIC host, but it hides a choice on hosts with more than one path out: a second WAN connection, a tailscale or wireguard interface, or any other locally running VPN. Three per-service compose fields make the choice explicit, so one service can take the overlay while everything else stays on the default WAN.

The three device pins

FieldWhat it pins
services[].uplink_devThe egress device the service's tunnel traffic leaves by. Applies to both the TCP and KCP transports.
services[].interface.parent_devThe parent device the service's MACVLAN interface is built on.
services[].routes[].deviceThe inbound interface a route matches traffic on, instead of the auto-detected WAN interface.

All three are host-local operator intent. They name devices on the service's source host, so they are set there and never gossiped to the rest of the cluster. All three are optional; leaving a field empty keeps the auto-detected behaviour.

uplink_dev is the field that moves traffic onto a different lane, and the examples below focus on it. interface.parent_dev matters when the service has a MACVLAN interface and the interface should attach to a specific NIC rather than the auto-detected one. routes[].device is the inbound counterpart: use it when the traffic a route should capture arrives on a second NIC or an overlay interface rather than the default WAN.

A service over tailscale

Forcing a service across an overlay takes two things, and the pairing is the part that is easy to miss: the target peer must publish its overlay address in its endpoints list, and the source service pins the lane with one line.

peers:
  - name: peer-sg1
    endpoints:
      - https://103.100.36.213:8080   # public
      - https://100.76.31.72:8080     # tailscale - must be
                                      #   published here

services:
  - name: svc-over-ts
    source_peer: peer-deb
    target_peer: peer-sg1
    transport_type: tcp
    uplink_dev: tailscale0            # the one line that forces
                                      #   the lane

Endpoint selection and the uplink pin work together. When a service carries an uplink pin, the engine probes the target's endpoints through the pinned device, so the endpoint that answers is the one reachable over the overlay. The link's egress is then derived from that selected endpoint, not from the host's default interface. That is what makes the pinned lane hold end to end: the pin steers the probe, the probe selects the overlay endpoint, and the selected endpoint determines the egress.

Both halves are required. A pin with no published overlay endpoint gives the probe nothing to reach through the pinned device, and a published overlay endpoint with no pin lets the probe select whichever path answers first.

Nothing here is tailscale-specific. The same shape works for wireguard (uplink_dev: wg0) or any other VPN interface that exists locally on the source host, as long as the target publishes an address reachable over that interface.

A second WAN NIC

The physical multi-WAN case is simpler, because the target is usually reachable over both paths and the pin alone decides which one this service uses:

services:
  - name: svc-via-wan2
    source_peer: peer-a
    target_peer: peer-b
    transport_type: tcp
    uplink_dev: ens19                 # the second WAN NIC

Other services between the same pair of peers keep the host's auto-detected default, so a bulk transfer can be moved off the primary WAN without touching anything else.

Changing pins on a live service

Both device pins can be changed without recreating the service:

arctic services update SERVICE_ID --uplink-dev tailscale0
arctic services update SERVICE_ID --interface-parent-dev ens19

# An empty value clears a pin and returns to auto-detection
arctic services update SERVICE_ID --uplink-dev ""

arctic services create accepts the same flags. Under compose, diff and apply pick up changes to the fields, and removing a field from the yaml clears the pin. Both fields appear in the arctic services get output, so a pin is always visible when inspecting a service.

Changed in v1.4.2

The device fields are new in v1.4.2. In a cluster that is mid-upgrade, a source peer still on an older version ignores the new fields on updates forwarded to it, so a pin set through a newer peer can appear to succeed without taking effect. Re-apply the configuration after the whole fleet is upgraded.

Multipath TCP

Once a host has more than one usable path, the next step is using several at once. A TCP-transport service can opt its tunnel dials into Multipath TCP, which spreads one tunnel across multiple subflows:

services:
  - name: bulk-sync
    source_peer: peer-a
    target_peer: peer-b
    transport_type: tcp
    mptcp:
      enabled: true
      subflows: 2
FieldDescription
enabledWhen true, tunnel dials request an MPTCP socket.
subflowsRequested path-manager subflow count. 0 means the engine default of 2; the maximum is 8.

The equivalent CLI flags are --mptcp and --mptcp-subflows on arctic services create and arctic services update. MPTCP is a TCP-transport feature: combining --mptcp with KCP transport is rejected at create time. Like the device pins, both knobs are source-peer-local dial settings and are not gossiped.

MPTCP pairs naturally with the device pins above: the pins give the host well-defined lanes, and MPTCP lets one service use more than one of them at the same time.

Kernel requirements

MPTCP needs kernel support on both peers: a kernel built with MPTCP, and the net.mptcp.enabled sysctl switched on. Check each host with:

sysctl net.mptcp.enabled

A value of 1 means MPTCP is available. If the key does not exist, the kernel was built without MPTCP support; some common kernels lack it entirely, including the LinuxKit VM kernel used by Docker Desktop.

Fallback to plain TCP is silent

When either end lacks MPTCP support, dials fall back to plain TCP by design. The service keeps working, so there is no error to notice. If throughput looks single-path on a service with MPTCP enabled, check kernel support on both peers before looking anywhere else.

See also

On this page