Dedicated machines
Give always-on backends a Hetzner VM of their own, one or several per machine. Sp00ky Cloud creates it, keeps it healthy, updates each app on it in place, and proxies traffic to them, so exposure and custom domains work as for any backend.
Dedicated machines are new. The shape of machines: is settled; what
spky deploy prints for them may still change.
A backend normally runs as a container on your deployment’s host: the shared host, or for a Pro project its own project host, next to the scheduler and the SSPs. On a Pro project the database has a VM of its own. A dedicated machine gives always-on backends a Hetzner VM of their own instead: its own CPU and memory, nothing of the platform on the box, and a fixed address. One backend can have a machine to itself, or several can share one.
Sp00ky Cloud owns the machine from start to finish: it creates it, keeps every app on it running,
updates each app in place when you deploy it, replaces the VM when it dies, and proxies traffic to
the apps, so deploy.expose, spky domain add --app, outbox jobs and the operator dashboard all work
exactly as for any other backend.
This is not a machine pool. A pool runs a backend for its jobs and takes the machines away when the queue is empty. A dedicated machine runs backends as services, always.
Declaring one
machines.<name> is the VM; runOn: { machine: <name> } on a backend puts it there. Any number of
backends may live on one machine, and runOn takes either pool or machine, never both. spky lint
checks all of it, and spky deploy refuses a control plane that does not offer dedicated machines
before building anything.
What a backend on it needs
deploy.port: the port the service listens on. Sp00ky Cloud forwards it (anddeploy.grpc_port, if set) to the machine through the core host.deploy.ports(optional): raw ports the machine opens to the internet on its own public address, for traffic the HTTP forwarder cannot carry: WebRTC media, RTMP, anything UDP. Each entry is published by docker on the VM and allowed in the machine’s firewall from anywhere ("7882/udp","7881","443:4433/udp", same syntax as inspky dev). They are not proxied by the core host, so clients need the machine’s public address:SPKY_MACHINE_IP(below) is it.deploy.healthcheck: an HTTP path answering 2xx when the service is ready. A new version of the app takes the place of the old one only once this answers.hosting: cloud(the default): Sp00ky Cloud runs it there. Ahosting: externalbackend has its own host already.- An image with a command: the VM imports the exported image, so
deploy.cmdis needed when the Dockerfile has noCMD.
Apps on one machine share its one address, so no two of them may use the same port: each app’s
port, grpc_port and deploy.ports entries must be distinct from every other app’s on that machine.
spky lint says which two collide.
The backend’s environment reaches the machine the same way as on the core host, with the usual
SPKY_* variables, except that SPKY_DB_URL and SPKY_DB_WS point at the published database
host (https://<slug>-db.<domain>) rather than the internal one, and SPKY_SCHEDULER_URL and
SPKY_SSP_ADDR are not set: those addresses have no public twin. Two more are added:
SPKY_MACHINE_IP: the machine’s public IPv4 address, for a service that has to advertise it (a WebRTC SFU, a QUIC server, an RTMP URL).SPKY_MACHINE: the machine’s name.
Apps on the same machine reach each other directly by name: every app is on the machine’s own
network as http://<app>:<port> (the recorder above talks to http://livekit:7880), without a trip
through the platform. deploy.resources is ignored; the machine type is the size, and the apps on
it share it.
What a deploy does
A machine has two kinds of change, and they are handled differently:
- An app changes (image, env, command, ports, healthcheck): only that app’s container is replaced, on the running VM. The other apps keep running untouched, and the machine keeps its address. The old container is stopped, the new one started, and the deploy waits until it answers its healthcheck. If it never does, the old version is put back, the deploy fails, and the machine keeps serving what it served before.
- The machine changes (
type,locations): a fresh VM boots beside the running one with every app on it, and traffic switches over once all of them answer; the old VM is retired after that. A machine that never gets there is destroyed, the deploy fails, and the previous machine keeps serving.fallbackTypesare not part of this: they only matter when a VM is made.
A new VM is started as soon as the deploy is accepted, so it boots while your migrations run. In the apps phase Sp00ky Cloud waits for the machine to run everything, then routes to it.
Each backend row is that backend’s presence on the core host: a small forwarder that carries the backend’s name and points at the machine. That is what keeps job routing, health probes and custom domains unchanged.
Updating an app in place is stop, then start: two copies of it cannot both hold its ports on one address. Expect a few seconds of that one app being unavailable per deploy. The first deploy of a new machine, or a change of its type, takes a few minutes longer: a VM has to boot and import the images.
What spky status shows
g3 is the generation: the third VM this machine name has had. The IP is the machine’s public
address. serving means traffic goes there; booting and healthy mark a newcomer that has not
taken over yet; unhealthy means the machine has stopped reporting and a replacement is on its way.
Under each machine is every app on it, as the machine last reported it.
Failure and replacement
An agent on each machine reports every few seconds what runs and whether it answers. When it stops reporting for three checks in a row (30 seconds apart), or the VM is gone at the provider, a replacement starts: a fresh machine with the same shape and every app, promoted the moment all of them answer. The one that failed is retired afterwards. This happens at most three times per hour per machine; past that the machine is reported down and you get the uptime alert email. The serving machine is never touched before its replacement is ready, so a false alarm costs a machine-hour, not an outage.
A crashing app is not a reason to replace the machine: docker’s restart policy on the machine brings it back, and its healthcheck is watched through the forwarder like any backend’s.
When Hetzner has no capacity for the machine type (or its fallbackTypes) in any of the listed
locations, a replacement cannot be made and the machine is reported down; Sp00ky Cloud keeps trying
every 30 seconds until capacity is back, and those attempts do not count against the three per hour.
Listing more than one location, and a fallback type or two, is the cheapest insurance against this.
spky restart <app> on a backend on a dedicated machine restarts that app’s container, on the same
machine; the other apps on it are not touched.
Every switch to a new machine recreates the small forwarder containers on the core host, which costs
about three seconds of 404 at the platform hostnames (measured on staging), the same as any other
backend’s container restart.
Local development
spky dev ignores runOn.machine. Each backend is started like any other app, from its dev:
command, and the local SSP dispatches its jobs to its baseUrl. There is no machine network in dev:
an app that talks to another app on its machine by name needs a dev: env pointing at that app’s
local address.
Limitations
- Hetzner only.
providermust behetzner; the machine type must be an x86 type the account can order. - Git-linked deploys place the backends and carry
deploy.portslikespky deploydoes;spky restart <app>remains a CLI action. - No logs from the machine yet.
spky logs <app>shows the forwarder’s log, not the backend’s. deploy.portsare not forwarded, onlyportandgrpc_portreach the machine through the core host; the others are open on the VM’s own public address.deploy.resourcesis ignored.- A backend that calls the scheduler (
SPKY_SCHEDULER_URL) does not work from the machine, as on pool machines. - Machines count against your plan’s machine allowance, together with pool machines, and bill per machine-hour. A machine shared by several apps counts once.
Reference
Machine fields
| Prop | Type | Default | Description |
|---|---|---|---|
| provider | hetzner | hetzner | Where the VM comes from. Only hetzner. |
| type | string | cx33 | Hetzner Cloud server type. x86 types only. |
| fallbackTypes | string[] | [] | Server types tried in order when type has no capacity or is over an account limit. Never replaces a running VM. |
| locations | string[] | [] | Hetzner locations in order of preference. Empty lets Hetzner choose. |
On a backend
| Prop | Type | Default | Description |
|---|---|---|---|
| runOn.machine | string | - | Name of the dedicated machine this backend runs on. Needs deploy.port and deploy.healthcheck. Several backends may name the same machine. |
| deploy.ports | string[] | [] | Raw TCP/UDP ports published on the machine's public address and opened in its firewall to everyone. Not proxied by the core host. Unique across the machine's apps. |