# Ships and docks

![Shipyard in the middle, three ships connected over gRPC with mutual TLS](/img/fleet.svg)

A **ship** is a worker on another machine — a Raspberry Pi next to the hardware it tests, a build box with more cores, a FreeBSD host. Ships connect to Shipyard, lease runs, execute them locally and send logs and results back. **Docks** in the console lists every ship.

## How it works

- Shipyard opens a fleet endpoint (gRPC over TLS, port 8443 by default) with its own certificate authority.
- You create a one-time **join token** in Docks. It is valid for one hour and carries the CA fingerprint, so the ship verifies it is talking to your Shipyard.
- `ship join` exchanges the token for the ship's own client certificate. From then on every call uses mutual TLS.
- A project with `runs_on` labels runs only on a ship that carries all of them. Projects without `runs_on` stay on the Shipyard machine.
- Credentials never travel: a run tells the ship *which* environment variable or file holds a key, and the ship reads it locally.

## 1. Open the fleet endpoint

Add to Shipyard's configuration and restart:

```yaml
runtime:
  api_token_env: SHIPYARD_API_TOKEN
fleet:
  listen: 0.0.0.0:8443
  address: shipyard.lan:8443          # what ships dial
  hosts: [shipyard.lan, 192.168.1.10] # names and IPs in the server certificate
```

Ships must reach `address`. Open TCP 8443 to them in your firewall. With Docker, publish `8443` next to `8080`.

## 2. Route a project to ships

```yaml
projects:
  - id: firmware
    repository: you/firmware
    enabled: true
    runs_on: [arch=arm64, site=garage]
    # …
```

Runs of `firmware` wait in the queue until a ship with both labels is free.

## 3. Install the ship

Download `ship` for the machine's platform from [Downloads](/downloads/):

```sh
sudo install -m 0755 ship-linux-arm64 /usr/local/bin/ship
sudo useradd --system --home-dir /var/lib/ship --shell /usr/sbin/nologin ship
sudo install -d -m 0700 -o ship -g ship /var/lib/ship
```

## 4. Join

In the console open **Docks → Add ship**, give it a name and labels, and copy the command.

![Add ship dialog showing the generated one-time join command](/img/screens/add-ship.webp)
 Run it on the new machine as the `ship` user:

```sh
sudo -u ship ship join -server shipyard.lan:8443 -token SYP1.… -name garage-pi
```

The ship writes its identity, private key and certificate to `/var/lib/ship`. The private key never leaves the machine. A token works once; create a new one for every ship.

## 5. Run it as a service

`/etc/systemd/system/ship.service`:

```ini
[Unit]
Description=Shipyard ship
Wants=network-online.target
After=network-online.target

[Service]
User=ship
Group=ship
StateDirectory=ship
StateDirectoryMode=0700
EnvironmentFile=-/etc/ship/ship.env
ExecStart=/usr/local/bin/ship run -state /var/lib/ship
Restart=always
RestartSec=5s
UMask=0077
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true

[Install]
WantedBy=multi-user.target
```

```sh
sudo systemctl daemon-reload
sudo systemctl enable --now ship
journalctl -u ship -f
```

Put the provider keys this ship's runs need into `/etc/ship/ship.env` (mode `0600`), e.g. `OPENAI_API_KEY=…`. A local model server on the ship (Ollama, vLLM) needs no key at all.

## Docks

![Docks view: two ships online, one offline](/img/screens/docks.webp)

| Column | Shows |
|---|---|
| Status | **Online** — called in within 15 seconds. **Offline** — not heard from. **Revoked** — no longer allowed. |
| Labels | What the ship carries for routing. |
| Platform, version | Reported by the ship. |
| Running | The run it is executing now, or Idle. |

**Revoke** cuts a ship off immediately: its certificate stops working, and a run it was executing is recovered like any interrupted run. To bring the machine back, join it again with a new token.

The run panel shows **Ran on** — the ship, or the Shipyard machine — for every run.

## Commands

```text
ship join -server HOST:PORT -token TOKEN [-name NAME] [-label key=value]... [-state DIR]
ship run [-state DIR]
ship version
```

`-state` defaults to `/var/lib/ship`. Labels given with `-label` are added to those in the token.
