Summary
Host cleanup is installed only by a site deploy. buildHostCleanupDeployScript is consumed in exactly one place — deploySiteRelease (compute-deploy.ts:95, spliced at :199 for PHP sites and :347 otherwise, with cleanUpAfterFailedDeploy at :207/:363). Any box role that never receives a site deploy therefore never gets /usr/local/bin/ts-cloud-host-cleanup.sh or ts-cloud-host-cleanup.timer, and nothing on it ever reclaims disk.
Verified on main @ f47d13d: grep -rn 'hostCleanup' packages/ts-cloud/src packages/ts-cloud/bin returns no hit in fleet.ts, in the rpx gateway path, or in any CLI command.
Which roles are uncovered
The fleet services box — buildFleetServicesBoxProvision() (fleet.ts:83). It gets buildAutoUpdatesScript(true), buildMonitoringScript(true), authorized keys, the notifier and the nightly DB backup, but no host cleanup. This is the worst case of the three, because it is where the data lives:
- it runs
unattended-upgradesdaily (fleet.ts:88), so it accumulates superseded kernels and/var/cache/apt compute.backupsputs the nightly DB backup here specifically ("MUST run here (where the database lives), never on an app box")- it therefore also holds
/var/backups/ts-cloud/databases, the directory #199 just gave retention to — but the on-demand path's retention only runs when someone triggers a dump, so a box whose dumps are all scheduled still has no timer-driven pass - it has journals from the backup cron and nothing vacuums them
The fleet LB box — reached by reloadRpxGateway (role: 'lb'), which runs its own script and never a site deploy. In a fleet topology rpx lives here, which is where /root/rpx-backups, cert-backup-* and rpx-rollback-* accumulate — the exact paths HostCleanupPathRule exists to handle. The path-rule feature cannot reach the box its motivating case lives on.
Any provisioned-but-not-yet-deployed box, for as long as that state lasts.
Why not provision time
Already settled and documented at compute-provision.ts:141: the ~7 KB install would push a typical PHP box's cloud-init past Hetzner's 32 KiB user_data limit, and a fresh box has nothing to clean. That reasoning holds; this issue is not asking to revisit it.
Suggested fix
server:optimize is the natural home. It is SSH-applied reconciliation of an existing host, so it has no user_data budget, and it already composes exactly this kind of builder — buildHetznerHostOptimizationScript (host-optimization.ts:185) splats swapScript, buildAutoUpdatesScript, buildMonitoringScript, hardeningScript and buildUfwScript. Adding the cleanup install is a one-line splat in the same list.
Two pieces:
- Install it in
buildHetznerHostOptimizationScript, readingcompute.hostCleanup. It is idempotent, so re-runningserver:optimizeis safe and an app box that already has it from a deploy is unaffected. - Verify it, mirroring the existing timer checks.
collectHetznerHostOptimizationReportalready derivesmetricsTimerActivefromsystemctl is-active ts-cloud-metrics.timer(host-optimization.ts:233); ahostCleanupTimerActivefromsystemctl is-active ts-cloud-host-cleanup.timerslots in beside it, with the matching assertion inverifyHetznerHostOptimizationgated on the resolved config being enabled.
That covers the LB and services boxes operationally. Worth considering separately whether buildFleetServicesBoxProvision and the LB provision should install it directly rather than depending on an operator running server:optimize — the 32 KiB limit is a PHP-app-box constraint and may not bind on those roles, but I have not measured their cloud-init size.
Note
server:optimize is itself undocumented, and docs/cli.md points operators at server:update / server:secure, which are registered as unsupported stubs that exit 2. So the remedy this issue proposes is currently a command nobody is told about. Probably worth its own docs fix.