VMs: sizing, disks, addressing, and access

A VM is a plain Linux machine on NVMe with one public IPv4, built from a stock image we keep patched. Nothing is installed on it that we chose — configuration is code you re-run. Every machine runs in our single site, in Los Angeles, US; there is no region field. For the walkthrough that gets you your first one, start with Onboarding.

Storage is documented here: a root disk is part of a VM, and a volume, though it can sit detached, is useful only in one of a VM's disk slots.

What you are running on

One site, Los Angeles, US. There is no region field and nothing to pick, and no second region to fail into.

AMD EPYC Milan (Zen 3). That is x86-64-v3 — AVX2 and everything below it, no AVX-512. Pick binaries on that baseline; an x86-64-v4 build faults on first use of an instruction the CPU does not have.

A 10 Gbps network, shared with other VMs. Egress is metered per GB and priced in the price book (GET /v1/billing/prices); inbound is free.

Storage is redundant on root disks and volumes alike, so you do not need to build replication. That is not backup. It does nothing about a delete you issued yourself, which is what snapshots are for.

Sizing: two numbers, and the CPU follows

POST /v1/vms {"name":"web1","ram_mb":4096,"disk_gb":40,
              "image":"ubuntu-lts","ssh_keys":["ssh-ed25519 AAAA..."]}

| Field | Minimum | Step | Notes | |-------|---------|------|-------| | ram_mb | 1024 | 1024 | max 262144 | | disk_gb | 24 | 8 | max 8192; never resizable after creation |

The vCPU count is derived, not chosen: ceil(ram_gb / 2). A 1–2 GB VM gets 1 vCPU, 4 GB gets 2, 16 GB gets 8. Omit cpu: the field is optional, and a value disagreeing with the formula is 400 cpu_derived. The price book (GET /v1/billing/prices) prices RAM and nothing for vCPU.

Resize with POST /v1/vms/{id}/actions {"type":"resize","ram_mb":8192}; the vCPU count follows. Stop the VM first — a resize on a running VM is 409 vm_not_stopped. You can size down as well as up, but nothing in the guest is adjusted for you: bring your services' own memory settings down before you shrink the machine, or they are killed on the way back up. The root disk is never resizable — see below.

Two kinds of disk

They differ in speed as much as in lifetime, and speed is usually what decides where something goes.

A database on the root with its backups on a volume is the normal shape.

The root disk is fixed at creation

disk_gb is the one decision you cannot change later. Oversize it — the fast tier prices above bulk storage (both are in the price book, GET /v1/billing/prices), and the extra GB still cost less than a recreate — and treat the root as disposable: a VM whose root is full or too small is recreated, not resized.

Root disks start at 24 GB and move in 8 GB steps. Round up.

Volumes

GET    /v1/volumes                     yours, attached or not
GET    /v1/volumes/{id}
POST   /v1/volumes                     {"name":"data1","size_gb":500}
POST   /v1/volumes/{id}/actions        {"type":"attach","vm_id":"vm_..."}
POST   /v1/volumes/{id}/actions        {"type":"detach"}
POST   /v1/volumes/{id}/actions        {"type":"resize","size_gb":1000}
DELETE /v1/volumes/{id}

A new volume arrives unformatted, and what you do with it once is in-guest: find it with lsblk (a disk with no partitions), make a filesystem on it, then read its UUID with blkid.

Mount it by UUID in /etc/fstab, never by device name. The kernel names disks in the order it finds them, and that order changes when you attach another volume or recreate the machine — so a /dev/sdb in fstab eventually mounts the wrong disk or none. Add nofail while you are there: without it a volume that is missing at boot stops the machine before SSH comes up, and there is no console to fix that from.

Snapshots

GET    /v1/vms/{id}/snapshot                    list, newest first
POST   /v1/vms/{id}/snapshot                    {"name":"before-upgrade"}
DELETE /v1/vms/{id}/snapshot/{name}
POST   /v1/vms/{id}/snapshot/{name}/rollback    newest only; VM must be stopped

The IP is fixed to the VM

Every VM gets one public IPv4 from our own pool at creation. It stays with that VM until the VM is destroyed, then returns to the pool. No endpoint reassigns an address.

It bills on the ip-hours meter, priced in the price book (GET /v1/billing/prices), for as long as you hold it, including while the VM is stopped. Deleting the VM returns it to the pool and ends the charge.

So put a DNS name in front of every VM rather than writing its address down anywhere. Keep the TTL low (60s is plenty) and a recreate costs a minute; both VMs exist during the cutover, so you can verify the new one before the old address goes away. If you need a specific address moved, open a ticket (POST /v1/support/tickets).

Your addresses are in GET /v1/vms — each machine carries its own ip. There is no separate address list.

Firewall: one platform rule, the rest is yours

The only rule we enforce is anti-spoof — a VM may emit only from its own IP and MAC. Nothing else is filtered and there is no firewall API. Run nftables or ufw in-guest; you own the policy.

Your VM's network reaches the Internet and nothing of ours. Everything arriving on your ports is yours to filter.

Two things the platform does decide for you:

Access is SSH, and only SSH

Pass at least one public key at creation — there is no password, no console, and no web reset path. Log in as ubuntu.

**A firewall or sshd change you haven't tested can brick the VM permanently**, with no out-of-band door to fix it through. Test changes before you commit them, and treat a locked-out VM as a recreate, not a rescue.

Lifecycle

GET    /v1/vms                yours, with power read live
GET    /v1/vms/{id}
POST   /v1/vms/{id}/actions   {"type":"start"|"stop"|"reboot"|"resize"}
DELETE /v1/vms/{id}
GET    /v1/vms/{id}/events    what happened, in order

A VM carries two separate fields, and there is no state: status is the platform's view of the record (active), and power is read live from the hypervisor on every request — running, stopped, or unknown when the hypervisor could not be reached, which health: unreachable says at the same time. power is never cached.

Creation blocks, and leaves the VM stopped: POST /v1/vms builds the machine and answers 201 with power: "stopped", typically in about a second. If it can't, you get the failure directly — 409 create_failed, carrying the hypervisor's own error text. Two refusals arrive before the build starts rather than from it: 409 no_capacity_available when there is no room for that much RAM right now, which is not a quota and not raised by raising one, and 409 ip_pool_exhausted when no address is free.

Creating and starting are separate calls. Attach before the first boot if your setup automation expects the disk to be there:

POST /v1/vms      {...}                             → 201, stopped
POST /v1/volumes  {"name":"data1","size_gb":500}    → 201, detached
POST /v1/volumes/{id}/actions {"type":"attach","vm_id":"vm_..."}
POST /v1/vms/{id}/actions {"type":"start"}

A failed create is rolled back: the partial VM is destroyed and its IP returned, so there is nothing to clean up and nothing holding quota. Fix what detail says and POST again.

Every action works the same way: it runs while you wait and the response is the outcome — 200 when it's done, or the error itself (the hypervisor's own text) when it isn't. There is nothing to poll; if a call fails, fix what detail says and issue it again. GET /v1/vms/{id} reports power live from the hypervisor whenever you want to look. stop is graceful with a ~20-second bound (pass "force": true for an immediate hard stop); start and stop are both idempotent, so a machine already in the state you asked for answers 200 rather than an error. Stopping a VM stops its RAM and vCPU metering and nothing else: the root disk, the address, its snapshots and its volumes bill whether it runs or not, so a machine you are done with is deleted rather than parked. GET /v1/vms/{id}/events records what happened, in order — it is where to look when a create fails. A VM that was running when the hypervisor under it rebooted comes back up on its own.

Deleting a VM destroys the VM and its root disk, and releases its IP. It destroys nothing else and tidies up nothing: delete the VM's snapshots first (newest first), then stop it and detach its volumes, then delete it. Until you do, DELETE answers 409 vm_has_snapshots or 409 vm_has_volumes and lists what is still there. The order matters — a volume can't be detached while one of the VM's snapshots includes it.

Images

Stock images only; no custom image upload. GET /v1/images is the catalog and the only source of truth for what is offered — Ubuntu LTS today, so check it rather than assuming. Pin an img_... id for reproducibility, or use a name alias like ubuntu-lts to follow the newest version under that name.

Images have no status and no expiry date. Everything listed is usable, and a withdrawn image stops being listed, so GET /v1/images is the whole answer.

Anything custom happens in-guest after boot.

What happens at zero balance

At balance ≤ 0, mutating calls stop (402 credit_cutoff) but **running VMs are not killed** and disks keep their data. There is a grace period before VMs are stopped and a longer one before any disk is deleted; top up inside it and everything resumes. The timings: API conventions.