Ships and docks

Shipyard in the middle, three ships connected over gRPC with mutual TLS

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:

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

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:

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 Run it on the new machine as the ship user:

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:

[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
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

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

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.