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.
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.
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>
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>
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.
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.
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.
Check the service network mode first:
yeet info <svc>
yeet ip <svc>
Use the mode to choose the next check:
svcandsvc,tsuse the catch host's gateway for ordinary internet access. Confirm the catch host itself can reach the internet.lanandlan,tsuse the DHCP gateway and DNS from the LAN or VLAN. Check that network's router, DHCP lease, and ACLs.- Plain
tsis tailnet-only unless you configure a Tailscale exit node. Addsvcor update the desired exit node withyeet service set <svc> --ts-exit=<exit-node>. isoallows 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.
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. svcorlancombined withiso.iso,tson a VM, native binary or script, or timer-backed cron job. Native and timer workloads support plainiso; only supported container-backed services can combine it withts.- 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.
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 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.
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.
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.
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.
yeet edit and yeet env edit launch an editor. Run them from an interactive
shell.
Inspect the staged payload:
yeet stage <svc> show
Apply it:
yeet stage <svc> commit
Or discard it:
yeet stage <svc> clear
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.
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.
yeet cannot reach catch
yeet init says catch needs a tag
yeet init got a different catch hostname
Docker payloads fail
Published port changes fail
Service networking does not work
A service has no outbound internet
An iso deployment is rejected
An iso workload cannot reach a private address
Yeet DNS names do not resolve
VM payloads do not start
Binary architecture mismatch
Native sandbox activation fails
edit fails without a TTY
Staged changes are confusing
copy destination errors
Service type mismatch