Status: parked. The Hetzner supervision model is undecided. Do not deploy the standalone systemd-per-daemon units or a convoy-owned replacement from this document until Nathan makes the architecture call.
The open architecture question is whether Hetzner should be supervised as:
systemd -> convoy up -> fabric + pty remote-serve + st-sync + agents
or as standalone systemd-owned daemon units such as:
systemd -> fabric
systemd -> pty remote-serve
systemd -> fabric pty-view exposure
This document is retained as a run-surface and diagnostic reference while that choice is pending.
These are the process commands and readiness checks any supervisor model needs to preserve.
Run surface:
~/.local/bin/fabric --home ~/.local/share/fabric daemon --allow-shellRequired readiness checks:
~/.local/bin/fabric statusThe status output must include:
shell allowed
This is a lockout check. A live daemon with shell disabled is not a healthy
Hetzner recovery state until pty remote attach fully replaces shell recovery.
Run surface:
PTY_ROOT=~/.local/state/convoy/pty \
~/.local/bin/pty remote-serve --socket "$XDG_RUNTIME_DIR/pty-remote.sock"Important constraints:
PTY_ROOTmust point at Hetzner's real session registry:~/.local/state/convoy/pty.- The remote socket must stay outside
PTY_ROOT; otherwise pty can mis-scan it as a phantom session. - pty is adding
pty remote-serve --print-systemd-unitas an authoritative source for the exact service surface. When that lands, prefer the generated command details over hand-maintained copies.
Run after both fabric and pty remote-serve are ready, and after the socket exists:
~/.local/bin/fabric expose pty-view --socket "$XDG_RUNTIME_DIR/pty-remote.sock"Required readiness check:
~/.local/bin/fabric statusThe status output must include pty-view in exposed.
The remaining sections are the pre-decision standalone systemd draft. They are useful for command surfaces and acceptance checks, but they are not active deploy instructions until Nathan chooses the supervisor model.
- Keep the Hetzner
fabricdaemon running under systemd with automatic restart. - Keep the pty remote-control server running under systemd with automatic restart.
- Preserve
fabric --allow-shelluntil pty remote attach fully replaces it. - Make deploys diagnosable: verify binary version before restart and verify local health after restart.
- Log to both journald and the existing app logs where practical.
- Avoid using an interactive shell,
nohup, detached helpers, or pty sessions as the long-term owner of either daemon.
- Do not deploy this while the fabric bus is down.
- Do not run the WAN drop test as part of supervisor installation.
- Do not replace pty's session-level persistence. systemd owns only the per-machine service processes; pty still owns PTY session state.
Use systemd user units for the Hetzner account that owns the fabric and pty state. Enable lingering once so the user manager starts at boot and survives logout:
sudo loginctl enable-linger "$USER"
loginctl show-user "$USER" -p LingerExpected:
Linger=yes
The units live in:
~/.config/systemd/user/fabric.service
~/.config/systemd/user/pty-remote-serve.service
Use fabric daemon directly so systemd owns the foreground daemon process. Do
not use fabric up, because up is a convenience command that can spawn a
background daemon and then exit.
Draft:
[Unit]
Description=Fabric daemon
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=%h/.local/bin/fabric --home %h/.local/share/fabric daemon --allow-shell
Restart=on-failure
RestartSec=2s
StartLimitIntervalSec=60
StartLimitBurst=10
KillSignal=SIGTERM
TimeoutStopSec=10s
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.targetNotes:
--allow-shellis intentionally explicit for the current Hetzner workflow. Long-term, move this policy into config so restarts do not depend on command memory.- The daemon still writes its existing file log under
~/.local/share/fabric/logs/daemon.logfor app-level diagnostics. Restart=on-failurerestarts crashes and non-zero exits. If we decide manualfabric downshould also be healed immediately, change toRestart=alwaysand usesystemctl --user stop fabricfor intentional stops.
The pty remote-control server should be owned the same way. pty confirmed that
pty remote-serve --socket <SOCK> is the long-running process systemd should
own directly with Type=simple.
Draft:
[Unit]
Description=pty remote control server
After=fabric.service
Wants=fabric.service
[Service]
Type=simple
Environment=PTY_ROOT=%h/.local/state/convoy/pty
ExecStart=%h/.local/bin/pty remote-serve --socket %t/pty-remote.sock
Restart=on-failure
RestartSec=2s
StartLimitIntervalSec=60
StartLimitBurst=10
KillSignal=SIGTERM
TimeoutStopSec=10s
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.targetNotes:
PTY_ROOTis critical. On Hetzner, pty sessions live under%h/.local/state/convoy/pty. If this environment variable is wrong, remote-serve will read the wrong registry and remotels/attachwill look empty even though the service is running.%t/pty-remote.sockexpands to$XDG_RUNTIME_DIR/pty-remote.sock, such as/run/user/<uid>/pty-remote.sock. Keep this socket outsidePTY_ROOT; a socket underPTY_ROOTcan be mis-scanned as a phantom pty session.- Use the absolute pty binary path actually installed on Hetzner. systemd does
not use an interactive shell
PATH. - Leave
PTY_REMOTE_SERVE_DEBUGunset in production unless diagnosing this service. Type=simpleis intentional. Do not background,nohup,setsid, or double-fork the process.
pty stays transport-agnostic, so fabric exposes the pty remote-control socket in
a separate one-shot unit on older fabric builds. Current fabric persists exposes
to <home>/config.toml, so re-running fabric expose pty-view ... once is
enough for fabric restarts after the socket path is stable. pty confirmed the
fabric protocol/ALPN is pty-view.
Draft:
[Unit]
Description=Expose pty remote control over fabric
After=fabric.service pty-remote-serve.service
Requires=fabric.service pty-remote-serve.service
PartOf=fabric.service pty-remote-serve.service
[Service]
Type=oneshot
ExecStartPre=/bin/sh -lc 'for i in $(seq 1 50); do test -S %t/pty-remote.sock && exit 0; sleep 0.1; done; echo "pty remote socket not ready: %t/pty-remote.sock" >&2; exit 1'
ExecStart=%h/.local/bin/fabric expose pty-view --socket %t/pty-remote.sock
RemainAfterExit=yes
[Install]
WantedBy=default.targetNotes:
- The
ExecStartPrewait avoids a startup race where systemd has started remote-serve but the Unix socket has not appeared yet. - Current fabric persists exposes by default. After restarting
fabric.service, verifyfabric statusstill listspty-view; no manual re-expose should be needed unless the target socket path changes. Usefabric unexpose pty-viewto retire the durable mapping. <home>/config.tomlis the durable daemon config for shell policy and exposes. The authoritative trusted-peer allow-list ispeers.toml.- The companion unit is only needed for older fabric builds without
<home>/config.tomlpersisted-expose support.
Run this only after Hetzner is reachable by SSH or a known-good control path.
- Diagnose before changing anything:
pgrep -af fabric || true
tail -200 ~/.local/share/fabric/logs/daemon.log || true
tail -200 ~/.local/share/fabric/logs/restart.log || true
uptime
df -h- Install the intended binaries.
For fabric, use the existing release installer or copy the built binary to:
~/.local/bin/fabric
For pty, install the matching pty build to:
~/.local/bin/pty
- Verify versions before restart:
~/.local/bin/fabric --version
~/.local/bin/pty --versionThe fabric version must include the expected short git SHA.
- Install or update unit files:
mkdir -p ~/.config/systemd/user
$EDITOR ~/.config/systemd/user/fabric.service
$EDITOR ~/.config/systemd/user/pty-remote-serve.service
$EDITOR ~/.config/systemd/user/fabric-pty-view-expose.service
systemctl --user daemon-reload- Enable lingering and services:
sudo loginctl enable-linger "$USER"
systemctl --user enable fabric.service
systemctl --user enable pty-remote-serve.service
systemctl --user enable fabric-pty-view-expose.service- Restart fabric first and verify locally before declaring the machine healthy:
systemctl --user restart fabric.service
systemctl --user status fabric.service --no-pager
journalctl --user -u fabric.service --no-pager -n 100
~/.local/bin/fabric statusThe fabric status output must include:
shell allowed
This is a lockout check, not cosmetic output. If shell shows disabled, the
daemon is alive but remote shell recovery is broken; fix the unit command or
the future shell policy config before declaring the deploy healthy.
- Restart pty remote-serve, expose it through fabric, and verify locally:
systemctl --user restart pty-remote-serve.service
systemctl --user status pty-remote-serve.service --no-pager
journalctl --user -u pty-remote-serve.service --no-pager -n 100
systemctl --user restart fabric-pty-view-expose.service
systemctl --user status fabric-pty-view-expose.service --no-pager
~/.local/bin/fabric statusThe fabric status output must include pty-view in exposed.
- From the Mac, verify remote reachability:
fabric ping hetzner
fabric statusDo not proceed to WAN drop testing until these checks are green.
When Hetzner is sick and SSH is available, capture cause before restarting:
date -Is
hostname
uptime
pgrep -af fabric || true
pgrep -af pty || true
tail -200 ~/.local/share/fabric/logs/daemon.log || true
tail -200 ~/.local/share/fabric/logs/restart.log || true
systemctl --user status fabric.service --no-pager || true
systemctl --user status pty-remote-serve.service --no-pager || true
systemctl --user status fabric-pty-view-expose.service --no-pager || true
journalctl --user -u fabric.service --no-pager -n 200 || true
journalctl --user -u pty-remote-serve.service --no-pager -n 200 || true
journalctl --user -u fabric-pty-view-expose.service --no-pager -n 200 || true
df -h
free -h || trueThen restart:
systemctl --user restart fabric.service
systemctl --user restart pty-remote-serve.service
systemctl --user restart fabric-pty-view-expose.service
~/.local/bin/fabric statussystemctl --user is-enabled fabric.serviceprintsenabled.systemctl --user is-active fabric.serviceprintsactive.systemctl --user is-active pty-remote-serve.serviceprintsactive.fabric statusworks locally on Hetzner after a service restart.fabric statusprintsshell allowedafter a service restart.fabric statuslistspty-viewunderexposedafter restartingfabric-pty-view-expose.service.fabric ping hetznerworks from the Mac after a service restart.- Killing the fabric process causes systemd to restart it:
# Ask systemd which pid it owns, then signal that pid alone.
pid="$(systemctl --user show -p MainPID --value fabric.service)"
[ -n "$pid" ] && [ "$pid" != 0 ] && kill "$pid"
sleep 3
systemctl --user is-active fabric.service
~/.local/bin/fabric statusDo not reach for pkill -f 'fabric .* daemon' here, or anywhere else. That
pattern matches every fabric daemon on the host, not the one you meant. This
repository's own dev workflow runs a second daemon on a separate FABRIC_HOME,
so the pattern would kill a colleague's dev instance, or a production daemon
while you were testing a dev one — and on a shared desk machine it can kill work
that is not yours at all. Ask the service manager for the pid, check it, signal
it.
- Rebooting Hetzner brings the fabric service back without SSH login, assuming lingering is enabled.
- Rebooting Hetzner brings pty remote-serve and the
pty-viewfabric exposure back without SSH login, assuming lingering is enabled.
- Simplify the Hetzner keepalive unit after rollout so config, not
daemon --allow-shell, is the source of shell policy. - Add
fabric doctorto collect process, log, version, and reachability facts. - Add
fabric statusfields for generic tunnel reconnecting state, attempts, last error, and buffered bytes before pty attach becomes user-facing over WAN. - Add richer
fabric exposestatus output that distinguishes socket, exec, and ephemeral targets.