Summary
There is no supported path for consolidating servers. Moving a deployed app from one server to another, hosting a second app on an existing server, and renaming a server are all manual today — which makes a routine "three boxes should be one box" cleanup an afternoon of SSH rather than a planned, reviewable operation.
The case that surfaced it
Three Hetzner servers, one app each:
| server | app | note |
|---|---|---|
statushq | StatusHQ | largest box of the three |
bughq | BugHQ | |
loghq | LogHQ |
The intended end state:
bughqandloghqmove onto thestatushqbox, attached as sites- that server is renamed to something like
hq-production-server - the two now-empty servers are deleted
- StatusHQ then monitors all three sites plus the host
Every step of that is a supported concept in ts-cloud's model except the transitions between them. You can stand up a server with an app on it; you cannot move an app to a different server, add an app to a server that already has one, or rename a server without recreating it.
What's missing
Three operations, in rough dependency order:
1. Attach an additional app to an existing server as a site. The current model is effectively one app per server. Consolidation is impossible without this: it needs per-site isolation (user, directory, service unit, log stream), its own TLS certificate and vhost, and its own env/secret scope on a shared host.
2. Move a deployed app between servers. Code, environment and secrets, persistent data, TLS, scheduled jobs and queue workers, then a DNS cutover — with the source left intact until the target is verified, so the operation is reversible up to the cutover point.
3. Rename a server in place. Purely an identity change (inventory record, hostname, DNS, provider label) that today implies destroy-and-recreate, which is not an acceptable cost for a naming-convention fix.
Requirements
To fit the control-plane principles in #132:
- Plan first.
--dry-runprints the operation graph — what moves, what is created, what is deleted, what DNS changes — before anything is touched. - Idempotent and resumable. A migration that dies halfway (network, token expiry, a full disk on the target) can be re-run and continues rather than starting over or double-applying.
- Typed confirmation for the destructive half. Deleting the drained source server is the one irreversible step and should be gated accordingly, separately from the move itself.
- Non-interactive. The whole sequence must be drivable from CI with flags and env vars only — this is the "fully automatable" requirement, not a nicety.
- Audited. Each step appends to the operation log with secrets redacted, so a migration can be reviewed after the fact.
- No hidden state. After the move, the config in the repo describes the new topology; the dashboard shows drift rather than silently reconciling.
Acceptance criteria
- An app can be attached to a server that already hosts another app, with isolated TLS, env, service unit and logs
- An app can be moved from server A to server B, including data and scheduled work, without a manual SSH step
- DNS cutover is part of the operation (Porkbun in this case) and is reversible until the source server is deleted
- A server can be renamed without being recreated
- The full sequence runs unattended from CI and is safe to re-run after a failure
--dry-runshows the complete plan, including deletions
Out of scope
Monitoring the resulting three sites and the host is StatusHQ's side of this and will be tracked separately. Provider credentials referenced here are HCLOUD_TOKEN for Hetzner and the Porkbun API keys for DNS — named only so the operation's inputs are clear.