Troubleshooting

Start from the symptom, then check the boundary that usually breaks.

Start with the symptom you see. If this is a fresh host, run First-Run Validation after fixing the first connection problem.

Most failures happen at a boundary: workstation to catch, catch to Docker, service to network, VM to KVM, or local config to live state. Debug the boundary first.

yeet cannot reach catch

Confirm you are targeting the catch hostname, not the SSH machine hostname:

CATCH_HOST=<catch-host> yeet status
yeet --host=<catch-host> status

Then check the layers in order:

  • The catch Tailscale node exists.
  • The node has a tag such as tag:catch.
  • Your device or admin tag can reach the catch tag on TCP port 41548.
  • The catch service is running on the host.

If you have the Tailscale CLI installed locally, this can help confirm the tagged node is visible:

yeet list-hosts

See Tailscale Setup for the tailnet policy shape.

yeet init says catch needs a tag

Catch rejects user-owned Tailscale nodes. It must join the tailnet with a tag-based identity. That keeps the daemon in the server side of the trust model.

Fix the Tailscale policy first, then rerun init:

yeet init root@<machine-host>

yeet init got a different catch hostname

Tailscale assigned a suffix such as catch-1 because another device already owns the requested hostname. This is not random naming. It is stale or conflicting state showing up as DNS.

Use the assigned catch host:

yeet --host=catch-1 status

Or remove the stale/conflicting Tailscale device and rerun yeet init.

If you are adding a second catch host, choose a distinct name instead of reusing the default catch name:

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

Docker payloads fail

Container payloads require Docker on the catch host. On Debian/Ubuntu-style hosts, interactive init can ask before installing it:

yeet init root@<machine-host>

If local image pushes do not show up on the host, rerun bootstrap and allow Docker setup. Yeet configures the Docker settings it needs during catch install.

For Compose services:

yeet info
yeet info <svc>
yeet logs <svc>
yeet ssh -- docker ps

See Containers for update and image workflows.

Published port changes fail

yeet service set protects the full published-port list. Include every mapping you want to keep. Otherwise you are not adding a port; you are replacing the list with a smaller one:

yeet service set vaultwarden -p 80:80 -p 443:443

To replace the list or clear it, use --publish-reset:

yeet service set vaultwarden --publish-reset -p 443:443
yeet service set vaultwarden --publish-reset

Use yeet info vaultwarden before changing ports.

Service networking does not work

First confirm the service has the network mode you expect:

yeet info <svc>
yeet ip <svc>

For svc networking, yeet info and yeet ip should show private service-network addresses. For lan, the host network must support macvlan and DHCP. A WAN-only VPS usually should not use lan. For iso, the output should show a ready isolation state, public egress, public-only DNS, and stable VM or component endpoints. A degraded or quarantined state includes the admission or reconciliation error Catch is refusing to ignore.

Apply an existing service's network change through service set:

yeet service set <svc> --net=<modes>

The command restarts and activates the service immediately. If activation fails, Catch attempts to restore the previous network. If rollback also fails, Catch stops the service or quarantines its network state instead of leaving a partially active network. Run yeet info <svc> again for the lifecycle state and error; a separate manual restart is not the network-activation step.

For a quarantined native iso service, yeet start <svc> and yeet restart <svc> fail before changing systemd state and preserve the diagnostic. Correct the reported boundary or runtime problem, then inspect the service before recovering the quarantined record manually on the Catch host:

yeet info <svc>

yeet stop <svc> does not clear quarantine. Verify the isolation boundary and active runtime before clearing the record by hand.

See Networking before changing modes on a shared host.

A service has no outbound internet

Check the service network mode first:

yeet info <svc>
yeet ip <svc>

Use the mode to choose the next check:

  • svc and svc,ts use the catch host's gateway for ordinary internet access. Confirm the catch host itself can reach the internet.
  • lan and lan,ts use the DHCP gateway and DNS from the LAN or VLAN. Check that network's router, DHCP lease, and ACLs.
  • Plain ts is tailnet-only unless you configure a Tailscale exit node. Add svc or update the desired exit node with yeet service set <svc> --ts-exit=<exit-node>.
  • iso allows globally reachable public IPv4 only. Private, link-local, metadata, CGNAT/Tailscale, special-purpose, and IPv6 destinations are expected to fail. Confirm Catch itself has internet access and the iso state is ready.

An iso deployment is rejected

iso fails closed before start when the payload or resolved Compose model could bypass the boundary. Read the field path in the error. Common causes include:

  • Published ports or Compose ports.
  • svc or lan combined with iso.
  • iso,ts on a VM, native binary or script, or timer-backed cron job. Native and timer workloads support plain iso; only supported container-backed services can combine it with ts.
  • Host networking, privilege or host-namespace changes, unsafe bind mounts or runtime sockets, custom DNS, extra networks, Catch-side builds, or replicas.
  • More than 29 active components or an unclassified Compose service field.

Do not remove the field merely to silence validation unless the resulting deployment still matches what the application needs. The Compose file is trusted deployment code; iso contains the admitted containers, not a definition that already asked the host runtime to escape containment.

An iso workload cannot reach a private address

That is the expected policy. iso allows workload-initiated public IPv4 egress, not arbitrary routing through Catch. It rejects Catch, LAN, svc, other iso projects, RFC1918, CGNAT/Tailscale, link-local, metadata, multicast, reserved, and IPv6 destinations. Components in the same admitted Compose project are the one private communication exception.

Use another network mode when the workload legitimately needs private access. For a supported container-backed service that needs a tailnet identity, use iso,ts. Native and timer workloads cannot use that combination. For a VM, install Tailscale inside the guest. Do not add svc or lan to iso; Yeet rejects those combinations.

Yeet DNS names do not resolve

Yeet DNS only publishes names for services and VMs using svc networking. Without a svc address, there is no record to publish.

Check the resolver from a service or VM on the service network:

dig @192.168.100.1 <svc> A +short
dig @192.168.100.1 <svc>.yeet.internal A +short

If the query returns nothing, confirm yeet info <svc> shows a service-network IP. See DNS for records and resolver behavior.

VM payloads do not start

VMs require a KVM-capable Linux catch host. Check the host:

yeet ssh -- sh -lc '
test -e /dev/kvm && echo kvm=yes || echo kvm=no
test -e /dev/net/tun && echo tun=yes || echo tun=no
for c in qemu-img zstd e2fsck resize2fs mount umount ip; do
  command -v "$c" >/dev/null && echo "$c=yes" || echo "$c=no"
done
'

If KVM is missing, use containers, binaries, scripts, and cron jobs on that host. If KVM and TUN/TAP exist but tools are missing on Debian/Ubuntu, interactive yeet init can ask before installing them:

yeet init root@<machine-host>

See VMs for image cache and console diagnostics.

Binary architecture mismatch

Build binaries for the catch host architecture. If the CPU cannot execute the file, the service cannot start it:

GOOS=linux GOARCH=amd64 go build -o ./bin/<svc> ./cmd/<svc>
yeet run <svc> ./bin/<svc>

Use the host's actual architecture when it is not amd64.

Native sandbox activation fails

Read the activation error before changing host policy. Catch refuses to fall back from sandbox state on to direct execution. Common causes are a missing or old Bubblewrap package, unsafe ownership or permissions on /usr/bin/bwrap, a disabled user-namespace path, or an AppArmor denial.

On an apt-managed Debian or Ubuntu host, refresh package metadata, install the package, and confirm the trusted path answers:

sudo apt-get update
sudo apt-get install -y bubblewrap
/usr/bin/bwrap --version

The version command is only a package check. Catch also runs a functional namespace probe under the workload's real UID and GID before activation.

Inspect user-namespace and AppArmor state without changing it globally:

sysctl kernel.unprivileged_userns_clone
sudo aa-status
sudo journalctl -k --grep='apparmor\|DENIED\|userns'

Some kernels do not expose kernel.unprivileged_userns_clone; use the Catch error and kernel security logs as the authoritative evidence on those hosts. If a compatible Ubuntu host enforces restricted unprivileged user namespaces, Catch manages the exact profile /etc/apparmor.d/yeet-bwrap. Inspect that path when Catch reports a conflict. Catch preserves different operator-owned content rather than replacing it.

If the distribution package is absent or too old, install an operator-managed compatible Bubblewrap that provides a trusted /usr/bin/bwrap. Yeet never disables AppArmor, changes a global user-namespace sysctl, or makes Bubblewrap setuid.

If the workload probe instead reports Permission denied for the service payload or data path, make sure the service identity can traverse every parent directory. Fresh Yeet-managed service roots already provide that traversal. On an older host whose services live below a private home directory, use a traversable host-controlled --service-root for a new service or plan a separate host-storage migration. Do not make the private home directory world-traversable just to satisfy the probe.

Path validation errors name the rejected source or destination. A read-only exposure can be a regular file or directory. A writable exposure must be a directory. Sources and destinations must be absolute, and destinations cannot overlap the fixed sandbox view or another exposure.

Use --sandbox=off only as a deliberate workload policy choice, not as a package-install workaround. When editing dormant exposures on an off service, repeat --sandbox=off in that command. Exposure flags without an explicit state result in on and run the Bubblewrap installation and activation checks. See Native Sandboxing for states, exposures, and the security boundary.

edit fails without a TTY

yeet edit and yeet env edit launch an editor. Run them from an interactive shell.

Staged changes are confusing

Inspect the staged payload:

yeet stage <svc> show

Apply it:

yeet stage <svc> commit

Or discard it:

yeet stage <svc> clear

copy destination errors

One side of a copy must be remote, such as <svc>:path or <vm>:path.

Examples:

yeet copy ./config.yml <svc>:config/config.yml
yeet copy ./configs/ <svc>:config/
yeet copy <vm>:"/var/log/*.log" ./logs/

Remote service paths are relative to the service data directory.

Service type mismatch

Yeet fixes a service type after the first install. If you installed a service as Compose, you cannot later replace it with a binary using the same service name.

Choose a new service name, or remove the old service first:

yeet rm <svc>

Use --clean only when you also want to delete the service data and remove the local yeet.toml entry. Data deletion is separate from retrying a remove.