> ## Documentation Index
> Fetch the complete documentation index at: https://orbit.nckrtl.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# cluster

> Group Nodes into a Cluster, give it a development TLD and one Router, and attach or detach member Nodes.

A Cluster is an optional group of Nodes with one name, at most one development TLD, and one Router. A Node outside a Cluster is standalone. Active membership gives a Node's Project Routes and [analytics tracking hosts](/docs/reference/analytics#publish-a-tracking-host) Cluster scope, so private traffic reaches them through the Router instead of the workload Node directly. [Custom proxy Routes](/docs/reference/routes#custom-proxy-routes) stay on their Node.

The [Routes reference](/docs/reference/routes) owns domain generation, routing scope, and Router transition ownership. [Private DNS](/docs/reference/private-dns#cluster-router-addresses) owns which Router address a requester receives.

## Commands

| Command | Result |
| - | - |
| [`cluster:create`](#orbit-clustercreate) | Create a Cluster, optionally with a TLD. |
| [`cluster:list`](#orbit-clusterlist) | List Clusters. |
| [`cluster:show`](#orbit-clustershow) | Show one Cluster with its members and Router. |
| [`cluster:update`](#orbit-clusterupdate) | Change the name, TLD, or state. |
| [`cluster:node:add`](#orbit-clusternodeadd) | Attach a Node. |
| [`cluster:node:remove`](#orbit-clusternoderemove) | Detach a Node. |
| [`cluster:router:set`](#orbit-clusterrouterset) | Set or replace the Router. |
| [`cluster:router:unset`](#orbit-clusterrouterunset) | Clear the Router from an inactive Cluster. |
| [`cluster:destroy`](#orbit-clusterdestroy) | Remove an empty Cluster. |

Every command accepts `--json`. Cluster and Node arguments are numeric IDs. The CLI refuses another Cluster value with `cluster.id_invalid`. Human lists use uppercase table headers, and one Cluster uses a detail tree. TLDs display with a leading dot; missing values display an em dash. Requests show progress while waiting and retain their Gateway request ID.

Destructive Cluster commands take `--force` as consent. The confirmation names the resolved Cluster and the affected Node when detaching a member. Interactive confirmation starts at No. Declining, Ctrl-C or EOF stops before mutation. A pipe or `--json` never supplies consent and requires `--force`.

A new Cluster is `inactive`. Membership, name, and TLD can be prepared while it is inactive without changing any traffic. Activating a Cluster that has a TLD, or giving an active Cluster a TLD, is the routing cutover: it requires exactly one active Router and reconciles every affected Route before the change becomes authoritative.

## Placement and TLDs

A Node belongs to at most one Cluster. A standalone Node is a full placement, not a step toward a Cluster. An Instance never selects a Cluster: its routing scope follows its Node. Joining a Cluster adds no role. Leaving it removes only a `router` assignment of the Node that never became active, as [`cluster:node:remove`](#orbit-clusternoderemove) describes.

A Node keeps its own TLD while it belongs to a Cluster, and membership never changes it. While the Cluster is active and has a TLD, that TLD wins for generated domains. Otherwise the Node TLD applies. [Routes](/docs/reference/routes#select-a-domain-and-scope) owns domain generation.

| Rule | Refusal |
| - | - |
| Node TLDs are unique among Nodes. | `node.tld_taken` |
| Cluster TLDs are unique among Clusters. | A validation error on `tld` |
| A Node outside an active Cluster cannot own that Cluster's TLD. Activating a Cluster with a TLD, or giving an active Cluster a TLD, checks this. | `cluster.tld_conflict` |
| A Node cannot take the TLD of an active Cluster that it is not a member of. | `node.tld_conflict` |
| A member that shares the active Cluster TLD cannot leave while that TLD is active. | `cluster.tld_conflict` |

An inactive Cluster owns no TLD, so it can share one with any Node. Prepare a Cluster with the TLD of a Node that joins it later, and activate it after the Node joins. A member Node can keep the same TLD as its active Cluster, because both name one routing scope.

## orbit cluster:create

Create a Cluster in the `inactive` state.

```bash theme={"dark"}
orbit cluster:create <name> [--tld=TLD]
```

| Argument | Required | Meaning |
| - | - | - |
| `name` | yes | Unique Cluster name. |

| Option | Default | Meaning |
| - | - | - |
| `--tld=TLD` | none | Development TLD as one DNS label, such as `test`. An active Cluster with this TLD owns generated development Route domains for its members. |

```bash theme={"dark"}
orbit cluster:create lab --tld=test
```

The CLI refuses a TLD that is not one lowercase DNS label with `cluster.tld_invalid` before it sends a request. [Placement and TLDs](#placement-and-tlds) lists the ownership rules.

## orbit cluster:list

List every Cluster with its state, TLD, Router, and member Nodes.

```bash theme={"dark"}
orbit cluster:list [--json]
```

## orbit cluster:show

Show one Cluster.

```bash theme={"dark"}
orbit cluster:show <cluster>
```

| Argument | Required | Meaning |
| - | - | - |
| `cluster` | yes | Numeric Cluster ID. |

## orbit cluster:update

Change the name, TLD, or state of a Cluster. At least one option is required.

```bash theme={"dark"}
orbit cluster:update <cluster> [--name=NAME] [--tld=TLD] [--state=STATE]
```

| Argument | Required | Meaning |
| - | - | - |
| `cluster` | yes | Numeric Cluster ID. |

| Option | Meaning |
| - | - |
| `--name=NAME` | New Cluster name. |
| `--tld=TLD` | New development TLD. An empty value, `--tld=`, unsets it. |
| `--state=STATE` | `inactive` or `active`. |

```bash theme={"dark"}
orbit cluster:update 3 --state=active
orbit cluster:update 3 --tld=
```

Activating a Cluster that has a TLD, or setting a TLD on an active Cluster, makes the Gateway check that one active Router exists, that no non-member Node owns the proposed TLD, and that every affected Route keeps a valid domain, scope, and target. Generated domains then use the Cluster TLD even when member Nodes have their own TLDs. The Gateway then republishes DNS and Router projections before it stores the change. A failure leaves the previous state authoritative. A name change alone touches no traffic.

| Error code | Meaning |
| - | - |
| `cluster.update_required` | No option was given. |
| `cluster.state_invalid` | The state is not `inactive` or `active`. |
| `cluster.router_required` | The Cluster would be active with a TLD but has no active Router. |
| `cluster.tld_conflict` | A Node outside the Cluster already owns the proposed TLD. |
| `cluster.router_busy` | Another Router transition still owns this Cluster; retry after it finishes. |
| `route.reconciliation_required` | An active Route depends on the change and cannot be reconciled by this operation. |

## orbit cluster:node:add

Attach an active Node to a Cluster.

```bash theme={"dark"}
orbit cluster:node:add <cluster> <node>
```

| Argument | Required | Meaning |
| - | - | - |
| `cluster` | yes | Numeric Cluster ID. |
| `node` | yes | Numeric Node ID. |

```bash theme={"dark"}
orbit cluster:node:add 3 7
```

Attaching to an inactive Cluster records membership and leaves Node routing and generated domains unchanged. Attaching to an active Cluster without a TLD uses Cluster scope and keeps generated domains on the Node TLD. Attaching to an active Cluster with a TLD moves generated Route domains into that Cluster namespace and Cluster scope before membership becomes authoritative. Custom proxy Routes on the Node keep Node scope and keep serving. `node:add --cluster=ID` attaches a Node during provisioning with the same guards.

| Error code | Meaning |
| - | - |
| `cluster.node_inactive` | The Node is not active. |
| `cluster.membership_conflict` | The Node already belongs to another Cluster. |
| `cluster.lan_ip_conflict` | The Node's LAN address is already assigned in the Cluster. |

## orbit cluster:node:remove

Detach a Node from a Cluster. Routes on that Node return to Node scope before membership ends. Generated domains fall back to the Node TLD. The Gateway deletes the Node's `router` assignments that never became active. Any other `router` assignment blocks the detach with `cluster.router_detach_forbidden`.

```bash theme={"dark"}
orbit cluster:node:remove <cluster> <node> [--force]
```

| Option | Meaning |
| - | - |
| `--force` | Skip the confirmation prompt. Required in non-interactive and `--json` calls. |

```bash theme={"dark"}
orbit cluster:node:remove 3 7 --force
```

| Error code | Meaning |
| - | - |
| `cluster.membership_missing` | The Node does not belong to this Cluster. |
| `cluster.router_detach_forbidden` | The Node is the Cluster Router; clear the Router first. |
| `cluster.ingress_detach_forbidden` | The Node carries the `ingress` role; remove that role first. |
| `cluster.tld_conflict` | The Node shares the active Cluster TLD and cannot leave while that TLD is active. |
| `cluster.confirmation_required` | The call was non-interactive without `--force`. |

## orbit cluster:router:set

Set or replace the Cluster Router. The Router must be an active member Node.

```bash theme={"dark"}
orbit cluster:router:set <cluster> <node>
```

| Argument | Required | Meaning |
| - | - | - |
| `cluster` | yes | Numeric Cluster ID. |
| `node` | yes | Numeric Node ID of an active member. |

```bash theme={"dark"}
orbit cluster:router:set 3 7
```

Replacement keeps the current Router active until its replacement is ready, promotes the candidate, and then cleans up the old assignment. Each Cluster has one Router operation owner. A concurrent Router change on the same Cluster waits up to 30 seconds and then fails with `cluster.router_busy`. A replacement that another Router change left unfinished fails with `cluster.router_transition_conflict`. A Node that is not an active member fails with `cluster.router_node_invalid`.

## orbit cluster:router:unset

Clear the Router from a Cluster that is inactive or has no TLD.

```bash theme={"dark"}
orbit cluster:router:unset <cluster> [--force]
```

| Option | Meaning |
| - | - |
| `--force` | Skip the confirmation prompt. Required in non-interactive and `--json` calls. |

<Warning>
  The Gateway refuses to clear the Router while the Cluster owns a Route (`cluster.routes_require_router`) or while the Cluster is active with a TLD (`cluster.active_router_required`). Deactivate the Cluster or remove its TLD first.
</Warning>

## orbit cluster:destroy

Remove an empty Cluster.

```bash theme={"dark"}
orbit cluster:destroy <cluster> [--force]
```

| Option | Meaning |
| - | - |
| `--force` | Skip the confirmation prompt. Required in non-interactive and `--json` calls. |

```bash theme={"dark"}
orbit cluster:destroy 3 --force
```

The Gateway refuses a Cluster that still has member Nodes (`cluster.not_empty`) or still scopes Routes (`cluster.has_routes`).

## Why it works this way

These reasons explain the design. Check them before you propose a change.

### Standalone Nodes

A useful one-Node fleet needs no Cluster, Router, or Ingress. A mandatory Cluster would make grouping a step before the first workload. So a Node can join a Cluster later, and its Instances keep their source and placement.

### The Node keeps its TLD

The Node TLD is the fallback when the Cluster becomes inactive, loses its TLD, or the Node leaves. Deleting it at attach time would leave detach with no namespace to return to.

## Related

* [`node`](/docs/cli/node) provisions the Nodes that join a Cluster and assigns the `router` and `ingress` roles.
* [`route`](/docs/cli/route) creates explicit Routes with Cluster scope.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.