---
title: Pilot Agent
description: Connect a remote Sencho node to your control instance through a single outbound WebSocket tunnel. No inbound ports, no TLS certificates, no public URL on the remote host.
---

Pilot Agent is one of Sencho's two remote-node modes. A small agent container runs on the remote host, dials your control instance over a single outbound WebSocket, and holds that connection open. Every request the control instance sends to that node rides through the same tunnel (HTTP, WebSocket, and, when that limited-availability surface is present, mesh TCP). The remote host never opens an inbound port and never needs a TLS certificate of its own.

This page is the architecture-and-operations reference for Pilot Agent. For the step-by-step UI walkthrough that adds a remote node, see [Multi-Node Management](/features/multi-node).

## How Pilot Agent fits Sencho

Sencho splits responsibilities between a **control instance** (the browser-facing Sencho your operators log into) and **nodes** (Docker hosts you manage from that control instance). The local node is always the control instance's own Docker socket. Remote nodes are other Sencho installations that the control instance manages on your behalf.

There are two remote modes, chosen per node when you register it:

<Frame>
  <img src="/images/pilot-agent/01-add-node-pilot.png" alt="Add remote node modal with Type set to Remote and the Mode dropdown open, showing the Pilot Agent option above the Distributed API Proxy option." />
</Frame>

- **Pilot Agent** (this page). The remote dials the control instance. Outbound HTTPS from the remote is the only network requirement.
- **Distributed API Proxy** ([documented under Multi-Node Management](/features/multi-node)). The control instance dials the remote. The remote must expose a reachable URL.

Both modes are first-class. They can coexist on the same control instance, one per node. Pilot Agent is part of Sencho's Community-tier core surface; there is no license gate on using it.

## When to choose Pilot Agent

Pilot Agent is the right choice when the remote host:

- Sits behind NAT or a residential router with no port forwarding.
- Lives on a corporate network that blocks inbound connections.
- Has a rotating cloud IP, a CG-NAT'd mobile uplink, or roams between networks.
- Has only outbound internet access (a typical homelab or office workstation).
- Would otherwise need a reverse proxy, dynamic DNS, or a self-signed TLS certificate purely to be reachable.

Choose **Distributed API Proxy** instead when the remote already has a stable, reachable URL (a VPS with a public IP, a LAN server with a static address, or a Tailscale node you have already terminated TLS on). The [Multi-Node Management](/features/multi-node) page compares both modes side by side.

You do not have to pick one mode for the whole fleet. Mix and match per node.

## How it works

Conceptually, the agent reverses the usual client/server direction.

1. The agent container starts on the remote host with its mode, control instance URL, short-lived enrollment token, and compose directory configured.
2. It dials `wss://<control-instance>/api/pilot/tunnel` and holds the WebSocket open for as long as the container runs.
3. For every request the control instance needs to make against that node (listing containers, deploying a stack, streaming logs, opening a console, forwarding mesh traffic), frames are multiplexed over that single connection.
4. On the agent side, those frames are demultiplexed and replayed locally against the host's Docker socket and filesystem.

Inside the control instance, the tunnel terminates at a **loopback bridge**: a tiny HTTP server on `127.0.0.1:<random-port>` per active tunnel. Every other Sencho feature (the existing HTTP proxy, WebSocket forwarder, mesh dialer, license-tier propagation) treats the bridge as just another remote URL. Once enrolled, the pilot node shows up in the same Nodes table, the same node switcher, and the same dashboard as a proxy-mode node, with the same shipped capabilities except where pilot mode intentionally narrows the surface (see below).

<Frame>
  <img src="/images/pilot-agent/03-nodes-table-tunnel.png" alt="Settings · Infrastructure · Nodes table showing a Local row and a Pilot Agent row. The pilot row has Mode badge 'Pilot Agent', Endpoint 'tunnel (seen 45m ago)', and Status badge 'Online'." />
</Frame>

What rides through the tunnel:

- HTTP requests (every API call and stream).
- WebSockets (live logs, container exec, notifications).
- Sencho Mesh TCP streams (cross-node service-to-service traffic).

What does NOT ride through the tunnel:

- Nothing inbound. The agent opens no listening port.
- The agent serves no UI of its own. Operators always work from the control instance.
- The agent's internal `/api/health` endpoint is reachable only from the tunnel's loopback bridge, not from outside the agent container.

### Intentional non-parity with proxy mode

Pilot mode narrows the operator surface in a couple of places where parity with proxy mode would be misleading:

- **Host console is not available on pilot nodes.** A pilot agent runs as a container with no useful shell into the underlying host, so the in-app host console is disabled at the capability level. If you need a host shell on a remote, use proxy mode for that node, or SSH in directly.
- **Self-update on pilot nodes recreates the agent container in place via the compose file the operator wrote during enrollment.** This is the same code path proxy-mode nodes use; it works as long as the host runs the compose file from the same directory across updates.

## Prerequisites

| Requirement | Detail |
|---|---|
| **Remote host** | Docker installed and a user that can run containers. About 1 GB free on the volume that backs `/app/data` and the selected compose directory. |
| **Network from remote → control instance** | Outbound TCP to the control instance's HTTPS port (whatever your reverse proxy or Sencho exposes). Nothing inbound. |
| **Network at the control instance** | The path from the remote to the control instance must allow WebSocket upgrades end-to-end. If you terminate TLS at a reverse proxy in front of Sencho, that proxy needs `Upgrade: websocket` passthrough. |
| **Operator role** | Admin on the control instance. The Settings → Nodes panel is admin-only. |
| **License tier** | None. Pilot Agent is Community-tier core surface. |

### The control instance's public URL

When the enrollment dialog generates the agent's compose file, it bakes the control instance's URL into `SENCHO_PRIMARY_URL` by reading the Host header on the admin's request. That works on a LAN, but it breaks when the pilot lives on a different network (a public cloud VPS, for example) and the admin happened to open the dialog at `http://127.0.0.1:1852` or a LAN address: the baked-in URL is then unreachable from the pilot.

Set `SENCHO_PUBLIC_URL` on the control instance to lock in a publicly-routable URL the dialog should always use:

```bash
-e SENCHO_PUBLIC_URL=https://sencho.example.com
```

Any HTTPS hostname reachable from the pilot works: a Cloudflare Tunnel, a reverse proxy, a port-forwarded home network with DDNS. The control instance validates the value (must be `http(s)://`, no loopback) and falls back to the request Host when unset.

## Enrollment and lifecycle

The end-to-end UI walkthrough for adding a Pilot Agent node (click paths, screenshots of the Add Node form, the Verify-the-tunnel step) lives in [Multi-Node Management](/features/multi-node#add-a-remote-node-pilot-agent). This section covers the underlying credential lifecycle, what happens during the connection, and how reconnects behave.

### Enrollment

When you submit the Add Node form with Mode set to Pilot Agent, the control instance:

1. Inserts the node into its registry with status `unknown` and no endpoint.
2. Mints a one-time enrollment JWT scoped to that node. The token's hash is stored on the control instance so it cannot be replayed; the token itself is returned to the operator and embedded in a generated Docker Compose file.
3. Shows the compose file in an enrollment dialog with a 15-minute countdown.

<Frame>
  <img src="/images/pilot-agent/02-enrollment-dialog.png" alt="Enroll the pilot agent modal with kicker 'NODES · PILOT ENROLLMENT'. Step 1 saves a Docker Compose file as 'compose.yaml' with the env block showing SENCHO_MODE: pilot, SENCHO_PRIMARY_URL: https://sencho.example.com, and SENCHO_ENROLL_TOKEN: <short-lived token>. Step 2 shows the 'docker compose up -d' command. Footer reads 'Expires 14m from now' alongside a 'Copy compose file' button." />
</Frame>

The enrollment token is one-time and short-lived by design. If a token is intercepted or leaked, the window of risk is small and the slot is consumed on first use.

The generated compose file looks like this, with the Pilot connection and compose path already configured:

```yaml
name: sencho-agent
services:
  agent:
    image: saelix/sencho:latest
    container_name: sencho-agent
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - sencho-agent-data:/app/data
      - type: bind
        source: /opt/docker/sencho
        target: /opt/docker/sencho
    environment:
      SENCHO_MODE: pilot
      SENCHO_PRIMARY_URL: https://sencho.example.com
      SENCHO_ENROLL_TOKEN: <short-lived token>
      COMPOSE_DIR: /opt/docker/sencho

volumes:
  sencho-agent-data:
```

Save it as `compose.yaml` on the remote host and bring the agent up from the same directory:

```bash
docker compose up -d
```

Deploying through Compose also lets the control instance push over-the-air updates to the agent later from the Fleet view ([Remote Updates](/features/remote-updates)), without manual intervention on the remote host. Compose-managed containers carry the labels Sencho needs to recreate them in place.

### First connect

The agent boots, reads its configuration, and dials `wss://<control-instance>/api/pilot/tunnel` carrying the enrollment token in an `Authorization: Bearer` header.

The control instance verifies the token, marks the enrollment slot consumed, and replies with a `hello` frame followed by a control frame that carries a **long-lived tunnel JWT** (365-day expiry). The agent writes that token to its data volume at `/app/data/pilot.jwt`. The control instance will not accept the same enrollment token again. Leave `SENCHO_ENROLL_TOKEN` in the compose file only as a recovery hook: if the control instance later rejects the persisted tunnel JWT at upgrade (for example after a secret rotation) **and** you have already put a **fresh, unexpired** enrollment token in the container environment, the agent retries with that enroll token and overwrites `pilot.jwt` only after a successful re-enroll. The original, already-consumed enroll token cannot recover the agent on its own.

The tunnel is now active. The Endpoint column in the Nodes table flips from `tunnel (waiting)` to `tunnel (seen Xs ago)` and the node's status badge turns Online.

### Subsequent reconnects

On every later boot, restart, or transient network drop, the agent reads `pilot.jwt` from disk and dials with that. No new operator action is needed. The persisted token is good for a year from the original enrollment.

The reconnect loop uses exponential backoff from 1 second to 60 seconds with up to 500 ms of jitter. The backoff resets only after a successful `hello` round-trip, so an agent that always fails the handshake (wrong URL, revoked token, version mismatch) cannot tight-loop and burn resources.

The control instance pings every connected agent every 30 seconds. Any stream with no activity for 10 minutes is closed and reclaimed.

### Primary restart, agent restart

- **Control instance restarts**: every agent's tunnel drops cleanly. Each agent reconnects within seconds. In-flight HTTP requests fail and the browser retries; long-lived WebSockets (logs, console) reconnect.
- **Agent restarts**: the persisted token re-authenticates without operator intervention. Same reconnect-and-recover behavior.
- **Operator deletes the node on the control instance**: the agent's existing tunnel is closed. The agent will keep trying to reconnect with its persisted token, which the control instance no longer accepts; the agent stays in a reconnect-backoff loop. To stop it, tear the agent down on the remote with `docker compose down -v`.

### Re-enrollment

If the agent container is destroyed before its first successful connect, the enrollment token expires unused, the control instance is restored from a backup with a different JWT secret, or you simply want to rotate the credential, open the node's Edit dialog and click **Regenerate enrollment token**.

<Frame>
  <img src="/images/pilot-agent/04-regenerate-token.png" alt="Edit node modal for a pilot-agent node showing a Re-enroll the agent helper text and a Regenerate enrollment token button." />
</Frame>

A fresh 15-minute token is issued and the previous tunnel (if any) is closed immediately so the new agent replaces it cleanly.

## Security and trust model

### Tokens

Two JWT scopes, both signed with the control instance's `auth_jwt_secret`:

| Token | Lifetime | Where it lives | Use |
|---|---|---|---|
| **Enrollment** | 15 min, one-time | Returned to the operator; SHA-256 hash stored on the control instance | Bootstraps the first connect |
| **Tunnel** | 365 days | Agent's container volume at `/app/data/pilot.jwt` | Every reconnect after the first |

Enrollment is rate-limited to 10 requests per minute per operator (per source IP if unauthenticated) so a runaway script cannot exhaust the database with pending tokens.

### TLS

The agent dials `wss://`. Certificate validation is on by default against the system trust store. There is no flag to disable verification; that would defeat the credential model. For internal CAs, mount your bundle into the agent container and set `SENCHO_PILOT_CA_FILE` to the in-container path. See [Self-signed control-instance TLS certificates](#self-signed-control-instance-tls-certificates) below for a worked example.

### Trust boundaries

- **The control instance trusts the agent.** A connected agent's frames are believed. A compromised remote can flood frames; the per-tunnel resource limits documented below bound the blast radius.
- **The agent trusts the control instance.** The control instance proves its identity by producing a valid JWT under the secret it shares with itself. A compromised control instance can ask the agent to do anything the agent could do directly on its host, and the agent holds the Docker socket, so that is effectively root on the remote.
- **Tier-trust headers (`x-sencho-tier`, `x-sencho-variant`) are only honored for bearer tokens with `node_proxy` or `pilot_tunnel` scope.** Browser session cookies cannot spoof them.
- **The agent exposes nothing inbound.** No port. No UI. No directly reachable health endpoint.

### What rotating the JWT secret does

The tunnel JWTs are signed with the control instance's `auth_jwt_secret`. If that secret rotates (because the control instance was rebuilt from scratch, restored into a different environment, or manually rotated), every existing tunnel JWT stops verifying. Regenerate enrollment for each affected node, update the agent compose file with the fresh token, and restart the agent containers. When a **fresh** `SENCHO_ENROLL_TOKEN` is present in the running container, the agent retries enrollment and overwrites `pilot.jwt` only after a successful `enroll_ack`; you do not need to delete the data volume by hand.

## Self-signed control-instance TLS certificates

If your control instance terminates TLS with an internal CA that the agent's system trust store does not recognise, mount the CA bundle into the agent container and point `SENCHO_PILOT_CA_FILE` at it:

```yaml
name: sencho-agent
services:
  agent:
    image: saelix/sencho:latest
    container_name: sencho-agent
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - sencho-agent-data:/app/data
      - type: bind
        source: /opt/docker/sencho
        target: /opt/docker/sencho
      - /etc/ssl/internal-ca.pem:/etc/ssl/internal-ca.pem:ro
    environment:
      SENCHO_MODE: pilot
      SENCHO_PRIMARY_URL: https://sencho.internal.example.com
      SENCHO_ENROLL_TOKEN: <short-lived token>
      SENCHO_PILOT_CA_FILE: /etc/ssl/internal-ca.pem
      COMPOSE_DIR: /opt/docker/sencho

volumes:
  sencho-agent-data:
```

The agent uses the bundle as the only trust anchor for the tunnel WebSocket. TLS verification stays on.

## Resource limits

Each pilot tunnel has fixed protocol-level ceilings so one misbehaving agent cannot impact the control instance or its peers.

| Limit | Value | What it caps | Why it matters |
|---|---|---|---|
| **Frame size** | 8 MB | Individual WebSocket frame, including a single HTTP request or response body chunk | A request or response body larger than 8 MB is rejected and the tunnel reconnects. Stream HTTP responses are chunked, so normal traffic stays well under this. |
| **Concurrent streams per tunnel** | 1024 | HTTP, WebSocket, and Sencho Mesh TCP streams share the same pool | New streams above the cap are rejected with 503 from the bridge and a typed error frame from the agent. A runaway script or a long-poll session that leaks streams can exhaust this; reload the affected tab or throttle the caller. |
| **Stream idle timeout** | 10 minutes | Per-stream inactivity before teardown | Idle WebSockets and forgotten HTTP streams are reclaimed automatically. |
| **System-wide tunnels** | 256, soft-warn at 128 | Concurrent pilot tunnels on a single control instance | Past the soft warning the control instance logs WARN. Past the hard cap, new tunnels are refused with WebSocket close code 1013 ("Try Again Later"); the agent backs off rather than tight-looping. |
| **Tunnel write buffer pause** | 4 MB | High-water mark for the agent-bound write queue | When the control instance's queue is over the mark, inbound HTTP request bodies are paused and resumed on drain. Backpressure rather than memory growth. |
| **Enrollment rate** | 10 per minute | Operator-issued enrollment tokens | Protects the database against a script accidentally minting tokens in a loop. |

An admin-only diagnostic endpoint, `GET /api/system/pilot-tunnels`, returns counters and a per-node breakdown of active tunnels (connect time and `bufferedAmount` for each). Useful for triaging a single sticky tunnel inside an otherwise healthy fleet.

## Environment variables

These environment variables are read by the **agent** container at boot.

| Variable | Required? | Default | Notes |
|---|---|---|---|
| `SENCHO_MODE` | Yes | none | Must be `pilot`. Putting any other value here causes the container to start as a normal Sencho instance, not as an agent. |
| `SENCHO_PRIMARY_URL` | Yes | none | The base URL of your control instance (e.g. `https://sencho.example.com`). The agent appends `/api/pilot/tunnel` and dials `wss://`. |
| `SENCHO_ENROLL_TOKEN` | First boot; keep for recovery | none | The 15-minute enrollment token issued by the control instance. During normal operation the agent dials with `pilot.jwt` instead. If that persisted token is rejected at upgrade (for example after a secret rotation), the agent falls back to this value only when it is still unexpired and unused; `pilot.jwt` is overwritten after a successful re-enroll, not deleted on the failed attempt. The original token left in compose after first enrollment cannot recover the agent. Update it in compose after regenerating enrollment on the control instance. |
| `SENCHO_PILOT_CA_FILE` | Optional | unset | Absolute path inside the container to a PEM bundle. Use when your control instance's TLS chain is rooted in a private CA. |
| `DATA_DIR` | Optional | `/app/data` | Where the persisted `pilot.jwt` is stored. Override only if you are mounting a different volume layout. |
| `COMPOSE_DIR` | Optional | `/app/compose` | Root directory where compose stack folders live. Enrollment sets it to the absolute path selected for the node and mounts that path identically on the host and in the agent. |

One variable is read on the **control instance** side, not the agent:

| Variable | Required? | Default | Notes |
|---|---|---|---|
| `SENCHO_PUBLIC_URL` | Optional | request Host | Locks in the URL the enrollment dialog bakes into `SENCHO_PRIMARY_URL`. See [The control instance's public URL](#the-control-instances-public-url) above. Must be `http(s)://`, no loopback. |

The agent does not currently expose tunables for the ping interval (30 s), the reconnect backoff (1–60 s), or the per-stream idle timeout (10 min). These are deliberate fixed values; if your environment requires different ones, open an issue.

## Limitations and non-goals

These are the boundaries operators should know about before designing a fleet around Pilot Agent.

- **No mode conversion.** The Edit dialog shows a Mode field for an enrolled node, but switching a node between Pilot Agent and Distributed API Proxy after enrollment leaves the credentials and connection state inconsistent. To change modes, delete the node and re-create it in the desired mode.
- **No audit log entries for enrollment lifecycle.** Node creation, enrollment regeneration, and node deletion do not write to the audit log today. This is on the roadmap.
- **`pilot.jwt` is not cleaned up on node deletion.** When you delete a node from the control instance, the agent's persisted token stays on the remote's data volume. The agent will fail to reconnect on next restart, but the file persists. If you are repurposing the host, tear the agent down with `docker compose down -v` to remove the `sencho-agent-data` volume.
- **JWT-secret rotation invalidates existing tunnel JWTs.** Rebuilding the control instance from scratch or rotating `auth_jwt_secret` requires a fresh enrollment token on each agent. When that fresh token is in the container environment, the agent re-enrolls automatically and overwrites `pilot.jwt` on success without requiring manual deletion of the file.
- **One tunnel per node.** Splitting a node's load across multiple control instances or running multiple agent containers against the same control instance for the same node is not supported.
- **Mesh and pilot share the per-tunnel stream pool.** A node that runs heavy Sencho Mesh traffic counts those streams against the same 1024-stream cap as HTTP and WebSocket traffic.
- **The agent has no UI of its own.** All operation flows through the control instance. The agent's container logs (`docker logs sencho-agent`) are the only direct visibility into agent-side behaviour.

## Sencho Mesh on Pilot Agent

When the limited-availability Mesh surface is present, mesh TCP can ride the existing pilot tunnel instead of opening a separate connection. Stack opt-in is per stack per node and is not affected by the transport choice. See [Sencho Mesh](/features/sencho-mesh) for the full mesh model.

## Troubleshooting

The generic node-connectivity issues (a node showing Offline, a pilot agent stuck on `tunnel (waiting)`, 401 Unauthorized on Test Connection) are covered in the [Multi-Node Management troubleshooting section](/features/multi-node#troubleshooting). The entries below cover failure modes that are specific to Pilot Agent.

<AccordionGroup>
  <Accordion title="Tunnel keeps disconnecting every ~30 seconds">
    The control instance pings the agent every 30 seconds; a reconnect that lines up with the ping interval almost always means an HTTP proxy or load balancer between the agent and the control instance is closing idle WebSockets faster than the ping cadence. Raise the proxy's WebSocket idle timeout above 30 seconds (a 90-second floor is safe). If you terminate TLS at a reverse proxy, also confirm that the proxy forwards the `Upgrade: websocket` header end-to-end.
  </Accordion>

  <Accordion title="Tunnel rejected with WebSocket close code 1013 (Try Again Later)">
    The control instance has reached the system-wide hard cap of 256 concurrent pilot tunnels. A reconnect storm or a runaway enrollment is the usual cause. Inspect the control instance's logs for the `[Pilot] Active tunnel count at soft limit` warning that fires at 128 to spot the trend, and check `GET /api/system/pilot-tunnels` (admin-only) for the per-node breakdown to identify the flapping agent. The agent backs off and retries automatically once headroom returns.
  </Accordion>

  <Accordion title="Loopback bridge returns 503 'pilot tunnel: stream cap reached'">
    A single tunnel has hit the per-tunnel 1024-stream cap. The most common trigger is a browser session that opened many long-poll streams without closing them, or a script firing parallel API calls without bound. Reload the affected tab so its WebSocket and SSE streams reset. If the cap is being hit by automation, throttle the caller. Sencho Mesh streams share the same pool, so a runaway mesh workload can also push the count up.
  </Accordion>

  <Accordion title="Tunnel closes with WebSocket close code 1002 (protocol error)">
    The agent sent a frame larger than the 8 MB ceiling, or the wire decoder rejected a malformed frame. Run the control instance with developer mode enabled (Settings → Operations → Developer Diagnostics) to surface the diagnostic decode log, then check the logs for `[PilotBridge:diag] Malformed frame from agent`. The agent reconnects automatically; persistent failures usually mean a version mismatch between the control instance and the agent image.
  </Accordion>

  <Accordion title="I ran the enrollment command on the wrong host">
    Tear the agent down on the wrong host (`docker compose down -v` from the directory you saved the compose file in), then regenerate the enrollment token on the control instance and deploy the new compose file on the correct host.
  </Accordion>

  <Accordion title="HTTP 429 'Too many enrollment requests'">
    The enrollment endpoint is rate-limited to 10 requests per minute per operator (or per IP when unauthenticated). A script accidentally minting tokens in a loop trips this. Wait 60 seconds and retry. To bulk-enroll many nodes, space the requests out.
  </Accordion>

  <Accordion title="Control instance was restored from backup and the agent will not reconnect">
    The persisted tunnel credential is signed with the control instance's `auth_jwt_secret`. If that secret was not part of the backup (or has been rotated for any other reason), existing tunnels stop verifying. Regenerate enrollment for each affected node from Settings → Nodes, update the agent compose file with the fresh `SENCHO_ENROLL_TOKEN`, and restart the agent containers. The agent completes enrollment and overwrites `pilot.jwt` when the new token is in the container environment.
  </Accordion>

  <Accordion title="Agent logs show Unexpected server response: 401 in a reconnect loop">
    An authenticating reverse proxy or IdP in front of the control instance often returns its own HTTP 401 on `/api/pilot/tunnel` (for example after a session cookie expires). That status never reaches Sencho's own auth, and the agent cannot re-enroll with the already-consumed token left in compose. Bypass or allow the agent Bearer upgrade for `/api/pilot/tunnel` (and the WebSocket upgrade headers) at the proxy. If the control instance's JWT secret truly rotated, regenerate enrollment, update `SENCHO_ENROLL_TOKEN`, and restart the agent; check the agent logs for the "re-enrollment required" message that names those steps.
  </Accordion>

  <Accordion title="I deleted the node on the control instance but the agent keeps trying to reconnect">
    Deleting the node removes the routing entry on the control instance but does not touch the remote. The agent still holds `/app/data/pilot.jwt` and will keep dialing. Tear the agent down on the remote (`docker compose down -v` from the directory holding the compose file) so the `sencho-agent-data` named volume that stores the persisted token is removed.
  </Accordion>

  <Accordion title="Stacks tab on a pilot node shows nothing or fails to list">
    The agent's compose directory is missing or not writable. Verify the selected absolute path exists, is owned by a user the agent container can write as, and is mounted at the same path on the host and inside the agent. The Compose Directory in Settings → Nodes controls the generated enrollment file.
  </Accordion>

  <Accordion title="I changed the Mode on a pilot node in Edit and now it is broken">
    The Edit dialog shows the Mode field, but switching from Pilot Agent to Distributed API Proxy (or back) after enrollment is not a clean flow; the persisted credentials and the new mode end up inconsistent. Delete the node, re-create it in the mode you want, and re-enroll or re-paste the API token.
  </Accordion>
</AccordionGroup>

## Common questions

<AccordionGroup>
  <Accordion title="Do I need port forwarding or a public IP on the remote?">
    No. The agent dials outbound; the remote needs no inbound port, no public IP, no DNS entry, and no TLS certificate of its own.
  </Accordion>

  <Accordion title="Is Pilot Agent the same as a VPN or Tailscale?">
    No. A VPN gives every host on the network a routable address for arbitrary peer-to-peer traffic. Pilot Agent is a single-purpose tunnel between one agent and one control instance, carrying only Sencho's own protocol. The two are complementary; you can run Sencho over a VPN (using Distributed API Proxy mode against the VPN address) if you prefer.
  </Accordion>

  <Accordion title="Does my data go through Sencho's servers?">
    No. The tunnel is a direct WebSocket between your remote host and your control instance. There is no Sencho-operated relay.
  </Accordion>

  <Accordion title="Can I run multiple agents on the same host?">
    Yes. Each agent needs its own container name and its own data volume so the persisted `pilot.jwt` files do not collide. Enroll each one against a distinct node entry on the control instance.
  </Accordion>

  <Accordion title="Does the control instance need a public domain?">
    No. The agent only needs to reach the control instance over whatever path your network provides: LAN, VPC, VPN, public internet, or a private tunnel. Set `SENCHO_PRIMARY_URL` to whatever URL works from the agent's vantage point.
  </Accordion>
</AccordionGroup>

## Related

- [Multi-Node Management](/features/multi-node): the operator walkthrough for adding remote nodes in either mode, the Nodes table reference, and the cross-mode comparison.
- [Sencho Mesh](/features/sencho-mesh): cross-node networking that rides on the pilot tunnel.
- [Fleet view](/features/fleet-view): fleet-wide aggregation and node health across both modes.
- [Security architecture](/reference/security): credential model and trust posture across the rest of Sencho.
