Host Setup

Bootstrap catch, configure Tailscale access, choose storage, upgrade hosts, and check container or VM support.

Host setup starts over SSH and then moves to Tailscale. yeet init installs catch on the machine host, enrolls catch in your tailnet, prepares storage, and checks optional host features such as Docker, ZFS, and VM support.

Common lookups

QuestionStart here
Which host name goes where?Host names
What is the default catch hostname?Host names
How do I install release or nightly yeet?CLI install
What Tailscale policy and OAuth setup are required?Tailscale access
What does yeet init prompt for?Bootstrap catch
When does Catch install Bubblewrap?Native sandbox dependency
How do I upgrade yeet and catch?Upgrade yeet and catch
Can this host run containers, VMs, or ZFS roots?Host requirements

Host names

Yeet uses two host names during setup:

  • <machine-host> is the SSH target for yeet init, such as root@server.example.
  • <catch-host> is the Tailscale hostname yeet uses after catch starts.

Use the machine host for installation. Use the catch host with CATCH_HOST, --host, and <svc>@<catch-host> after installation. The machine name answers "how do I SSH there?" The catch name answers "which daemon am I operating?"

The default catch hostname is catch. That is fine for one host. For more than one catch host, choose a name before setup:

yeet --host=morpheus-catch init root@<machine-host>

That command still installs over SSH to <machine-host>. The --host value is the catch hostname yeet asks Tailscale to assign. Use that same catch host for normal commands:

yeet --host=morpheus-catch status
yeet run <svc>@morpheus-catch ./compose.yml

To make that catch host the local default:

yeet config --host=morpheus-catch

During setup, SSH gets catch onto the machine. After that, Tailscale is the control path. Mixing those names up is the fastest way to make a working host look unreachable.

CLI install

Release install:

curl -fsSL https://yeetrun.com/install.sh | sh

Nightly build:

curl -fsSL https://yeetrun.com/install.sh | sh -s -- --nightly

Confirm the CLI is available:

yeet --help

Tailscale access

Catch joins your tailnet as a tagged device, such as tag:catch. This is required before the first yeet init and keeps catch tied to server-side policy.

Your tailnet policy must also grant the setup user access to catch on TCP port 41548 with the yeetrun.com/app/yeet app permissions read, manage, and ssh. First setup requires all three. Later, you can split access into separate read, manage, and shell roles.

Before you run yeet init, create an OAuth credential in the Tailscale admin console:

Trust credentials -> Credential -> OAuth

You have two choices:

  • Simple broad setup: choose All - Read & Write if you are comfortable giving the credential broad Tailscale API access.
  • Least-privilege setup: choose custom scopes with Auth Keys write (auth_keys) and select the tag the credential may assign. For catch-only installs, select tag:catch directly. If you plan to use service --net=ts later, use an owner tag such as tag:yeet that owns tag:catch and service tags such as tag:app.

Keep the tskey-client-... OAuth client secret ready. Interactive yeet init asks for it during first setup. This secret creates the catch node credential, so do not lose track of where it came from. Follow Tailscale Setup for the minimal policy snippet, and use Tailscale Access Grants when you are ready to split roles.

Before deploying a service with --net=ts, read Networking. Most Tailscale-exposed services should use svc,ts so they keep yeet DNS and ordinary outbound internet through the catch host.

Bootstrap catch

Normal interactive setup runs from your workstation against the SSH machine host:

yeet init root@<machine-host>

With no saved or explicit catch host, this uses the Tailscale hostname catch. Use yeet --host=<catch-host> init root@<machine-host> when adding another catch host to the same tailnet.

Choose the workspace behavior explicitly when you do not want the interactive default:

yeet init --workspace ~/yeet-services root@<machine-host>
yeet init --no-workspace root@<machine-host>

--workspace creates the directory when needed and registers it for the selected catch host. --no-workspace skips workspace setup entirely. In non-interactive runs, workspace setup only happens when --workspace is provided.

What changes by case:

CaseWhat yeet init does
First catch enrollmentPrompts for the Tailscale OAuth client secret.
Fresh storage setupUses /var/lib/yeet for Catch state, with services under /var/lib/yeet/services.
Native service accountCreates the unprivileged yeet-svc account when it is not already present.
Existing catch installUpgrades catch. An exact recorded legacy layout can be offered the guided move described below.
Missing Debian/Ubuntu packagesAsks before installing supported dependencies such as Docker or VM tools.
Non-root SSH userRuns the remote install with sudo.
VM LAN bridge setupCan prepare the host bridge when you plan to use VM --net=lan.

The data directory is where live host state begins to accumulate, so treat it as infrastructure, not cache.

New native binaries, scripts, and cron jobs run as the shared yeet-svc account by default. Yeet lets the host allocate that account's UID and GID; it does not assume that UID 1000 is available or has the right meaning. Existing native services are not changed during init or upgrade. Migrate one explicitly with yeet service set <svc> --run-as=yeet-svc, or select an operator-created account with --run-as=<user>[:<group>].

Only the native workload unit drops privilege. Catch, Tailscale, VM and networking helpers, and storage migration helpers continue to run with the host privileges they require. Docker users remain controlled by Compose, and VM application users remain controlled inside the guest.

Native sandbox dependency

Fresh native binaries, shebang scripts, and scheduled jobs run through Bubblewrap. Catch installs and functionally probes /usr/bin/bwrap without an extra prompt at three intent boundaries:

  1. A genuinely fresh Catch installation.
  2. A new native run or another native activation whose resulting sandbox state is on.
  3. yeet service set when the resulting sandbox state is on.

Installing or upgrading Yeet does not install Bubblewrap. Upgrading an existing Catch installation does not install it either. Existing legacy services, services explicitly set to off, and exposure edits that explicitly repeat --sandbox=off in the same command do not require it. An exposure-only edit of an off service results in on, so Catch installs and probes Bubblewrap before activation. See Native Sandboxing for the exact dormant-edit command.

Catch verifies ownership and permissions on the binary and runs a functional namespace probe as a fixed non-root identity. On compatible Ubuntu hosts where AppArmor restricts unprivileged user namespaces, Catch installs and loads the exact Yeet-owned profile at /etc/apparmor.d/yeet-bwrap, then repeats the probe. Debian and hosts without that restriction need only the Bubblewrap package. A different file at the managed path is preserved and reported.

Catch never disables AppArmor or relaxes host-wide user-namespace policy. See Native sandbox activation fails for package and policy checks.

The service root defaults to <data-dir>/services. Pass --data-dir or --services-root when the host needs a different filesystem path. Explicit custom roots are preserved during init, upgrades, and guided migrations. Put a custom service root below host-controlled directories: each parent must be root-owned and not group- or world-writable. /srv/apps and ZFS mountpoints are appropriate; a workload-owned home directory is rejected.

If the host has ZFS, setup can use datasets and create missing child datasets when the parent exists. Use this only when you want the host storage on ZFS:

yeet init --zfs --data-dir=flash/yeet/data --services-root=flash/yeet/services root@<machine-host>

ZFS datasets are not copied or deleted implicitly. A custom dataset and any nested datasets remain where they are unless you manage them explicitly.

If you plan to run VMs with --net=lan, answer yes when init asks to prepare the host bridge. If you skip it, the first yeet run <vm> ... --net=lan prompts before creating the VM service.

Many VPS providers do not expose nested virtualization. That is a host constraint. On hosts that are not VM capable, yeet warns and containers, binaries, scripts, and cron jobs still work.

Confirm and validate

After bootstrap, confirm yeet can reach catch:

yeet version
yeet status

If this is not your default catch host, pass it explicitly:

yeet --host=<catch-host> status

Then run the First-Run Validation playbook to test the payload types this host should support.

Upgrade yeet and catch

Check the local CLI and catch hosts:

yeet upgrade check

Upgrade from verified GitHub release assets:

yeet upgrade

When you run from a service workspace with yeet.toml, yeet upgrade includes all project catch hosts plus the default catch host. Use --host=<catch-host> only when you want to upgrade one catch host. Otherwise, the project file is the source of truth and yeet follows it.

yeet upgrade --host=<catch-host>

To reinstall the latest public release even when a component already looks current, newer, or locally built:

yeet upgrade --force

To install the latest nightly release:

yeet upgrade --nightly

To install a specific public release, select the tag:

yeet upgrade --version v0.6.1 --force

--nightly and --version select different targets, so use one of them per command.

Move the exact legacy layout

An interactive init or upgrade can offer to move the recorded legacy home-directory layout to /var/lib/yeet. Accepting the prompt moves Catch state and eligible service directories, restarts Catch, and validates the new state. After validation, it removes the exact legacy tree you approved in the prompt.

When init could not prompt, run the migration explicitly from your workstation:

yeet init root@host

# Explicit migration when init could not prompt:
yeet host set \
  --data-dir=/var/lib/yeet \
  --services-root=/var/lib/yeet/services \
  --migrate-services=all \
  --yes
yeet host cleanup --from=/root/yeet-data --yes

Run yeet host cleanup only after yeet host set reconnects and reports that the new state is valid. Cleanup refuses arbitrary paths. It accepts only the exact journaled legacy source and revalidates the active Catch and service state before deletion. If only deletion fails, rerun the same cleanup command; the pending cleanup resumes safely.

Host requirements

Base host:

  • Linux with systemd.
  • SSH access from your workstation.
  • A catch tag allowed by your Tailscale policy.

Native payloads:

  • A compatible Bubblewrap at the trusted /usr/bin/bwrap path.
  • Kernel and host security policy that allow the functional user-namespace probe. Catch installs the package automatically on supported apt-managed Debian/Ubuntu hosts at the intent boundaries above.

Container payloads:

  • Docker on the host.
  • Docker configured for local image pushes. yeet init handles this during catch install.

VM payloads:

  • x86_64/amd64 Linux.
  • KVM at /dev/kvm.
  • TUN/TAP at /dev/net/tun.
  • qemu-img, zstd, e2fsck, resize2fs, mount, umount, and ip.

ZFS is optional. Use it when you want dataset-backed host storage, service roots, snapshots, or fast VM disk clones. Skip it when a normal filesystem path is enough.

See Data Layout and ZFS.

Manual catch install

Use yeet init unless you have a specific reason to install catch manually. Manual install has fewer guardrails.

If you do install catch yourself, see the catch reference for catch flags such as --data-dir and --tsnet-host.