Skip to content
sitesolide

Docs A machine

A machine from a cloud provider

sitesolide machine orders the VM that sitesolide setup then installs, from the provider's API, with nothing to install on the workstation and no state file to keep. Hetzner Cloud is the only provider for now.

$ read -rs HCLOUD_TOKEN && export HCLOUD_TOKEN      # paste the token: nothing is echoed
$ sitesolide machine create --provider hetzner --name web
-> machine web at Hetzner: cx23 at fsn1, debian-13
...
-> ready
   name      web
   type      cx23 at fsn1
   ipv4      203.0.113.10
   ipv6      2001:db8:10::1, network 2001:db8:10::/64

next: sitesolide setup root@203.0.113.10 --zone <your zone> --email <you>

Make one in the Hetzner console: open the project, then Security, API tokens, Generate API token, with the Read & Write permission. A Read token lists machines but cannot create or delete one.

Give sitesolide a project of its own. A token reaches everything in its project and nothing outside it: in a project that holds only this machine, the token can touch nothing else you run at Hetzner, and the console shows at a glance what sitesolide costs you.

The token is read from HCLOUD_TOKEN, the variable Hetzner's own tools read, or from standard input with --token-stdin:

<your password manager's read command> | sitesolide machine list --provider hetzner --token-stdin

It is never an option: an option sits in the shell's history and in what ps shows to every user of the workstation. sitesolide sends it in the Authorization header of Hetzner's API and nowhere else, never writes it to a file, and never prints it: should a message ever carry it, it is replaced with [token] before it is printed.

Three resources, each labelled managed-by=sitesolide and sitesolide-machine=<name>. The labels are how list finds them and how destroy tells them from what it must never touch.

ResourceWhat it is
SSH keyYour workstation's public key: --ssh-key, else the first of ~/.ssh/id_ed25519.pub, id_ecdsa.pub, id_rsa.pub. A key the project already holds, under any name, is reused; otherwise it is uploaded as sitesolide-<name>.
Firewall <name>Inbound TCP 22, 80 and 443, and ICMP, from 0.0.0.0/0 and ::/0; everything else inbound is dropped before it reaches the machine.
Server <name>cx23 at fsn1 with debian-13 unless --type, --location and --image say otherwise, a public IPv4 and IPv6, the key for root, the firewall. --backups turns on Hetzner's daily backups.

Before anything is created, create reads what could refuse it: a server of that name sitesolide did not create (it is never touched: pick another name), a type the location does not sell or no longer sells (the refusal lists the cheapest ones it does, with their price), a firewall of that name that is not sitesolide's. A refusal leaves nothing behind.

Hetzner's list of types also says whether a type is available at a location, and that flag does not decide: it has been seen false for a type Hetzner then created at once. A type listed as unavailable is ordered all the same, after a warning (Hetzner lists cx23 as unavailable at fsn1; trying anyway). If Hetzner refuses the order itself, the refusal lists the cheapest types it lists as available there, or suggests another --location, and the SSH key and the firewall this run had created for the server are deleted again: without a server, nothing in sitesolide would ever list or remove them. What an earlier run made, or the project already held, is left as it was; a deletion that fails leaves the resource for the next run, which reuses it.

It then waits for Hetzner to finish creating and starting the server, then for port 22 to accept a connection, each wait bounded, and prints the setup command that comes next. Every step is idempotent: a run interrupted anywhere, or a wait that timed out, is finished by running the same command again, which finds what the first run made.

It installs nothing and hardens nothing. The server carries no cloud-init user_data: sitesolide setup does the hardening over SSH, as root, where it can be checked and run again.

Hetzner bills the server from its creation until its deletion, by the hour up to a monthly price; the public IPv4 address is billed on top of it, and backups add 20 % to the server's price. The prices are on Hetzner's pricing page and, for your project and its currency, in the console. machine list shows the monthly price the API gives for each machine, before VAT, and a refused --type lists the cheapest types of the location with theirs.

sitesolide machine list --provider hetzner

The servers labelled managed-by=sitesolide, with their type, location, status, addresses and monthly price. Servers sitesolide did not create are not listed.

sitesolide machine destroy web --provider hetzner

At a terminal, the name is typed back to confirm; anywhere else, --confirm web says it, and a value that is not the name stops everything. A server without the managed-by=sitesolide label is refused: delete it from the console if it has to go.

The server goes first, with its disk and the Hetzner backups of it, which the API deletes with the server. Its firewall goes next, unless it is still applied to another server. The SSH key stays, since other machines may be created with it; --delete-key deletes it too, and only when it was uploaded for this machine. The command ends by listing what it removed and what it kept.

DNS records are not touched. They live in your zone, not at Hetzner: delete the A and AAAA records that pointed at the machine yourself, or they keep naming an address Hetzner will give to someone else.

--json prints one JSON event per line and ends with one result or one error carrying a hint, as every other command does (agents.md). destroy never prompts under --json: it needs --confirm <name>, which an agent passes only when the owner asked for that machine to be destroyed.

SITESOLIDE_HETZNER_API points the command at another address than https://api.hetzner.cloud/v1, for a local fake of the API (bin/tests/fake-hetzner.ts). It takes https, or http on the loopback only, so that the token never travels in the clear.