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. We install nothing else on it. For the walkthrough that gets you your first one, start with Onboarding.

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 over to.

AMD EPYC Milan (Zen 3). That is x86-64-v3 — AVX2 and everything below it, no AVX-512. Pick binaries built for that level; an x86-64-v4 build crashes the first time it uses 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 and retention are for.

Sizing

POST /v1/vms {"name":"web1","ram_mb":4096,"disk_gb":40,
              "image":"ubuntu-lts","ssh_keys":["ssh-ed25519 AAAA..."]}
FieldMinimumStepNotes
ram_mb10241024max 262144
disk_gb248max 8192; grows, never shrinks

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) charges for RAM and not for vCPU.

Resize with POST /v1/vms/{id}/actions {"type":"resize","ram_mb":8192}; the vCPU count changes with it. The same call can grow the root disk too ("ram_mb":8192,"disk_gb":80; [growing the root disk](#growing-the-root-disk)). 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, either way: raise your services' own memory settings (a JVM heap, Postgres shared_buffers) after you grow the machine, and lower them before you shrink it, or the kernel kills them when the VM starts again.

Two kinds of disk

The usual setup: a database on the root disk, its backups on a volume.

Growing the root disk

POST /v1/vms/{id}/actions {"type":"resize","disk_gb":80} grows the root of a stopped VM. At the next start, cloud-init in the stock image grows the partition and filesystem to fill it; it does this on every boot, so it needs nothing from you unless you have removed or disabled cloud-init.

A root never shrinks: a smaller disk_gb is 400 grow_only, and a smaller root means recreating the VM.

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 has no filesystem. Set it up once, in the 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 only rescue gets you back in.

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 switching to a recreated VM takes about a minute; both VMs exist during the switch, 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

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

Your VM's network reaches the Internet and none of our internal systems.

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. Log in as ubuntu.

A firewall or sshd change you haven't tested can lock you out: there is no console, and the way back in is rescue. Test changes before you keep them.

Rescue

A VM in rescue boots a live Ubuntu Server system instead of its own disk, and GET /v1/vms/{id} shows "rescue": true. The rescue and unrescue actions on POST /v1/vms/{id}/actions switch it; they need the VM stopped, and the switch takes effect at its next start:

  1. Stop the VM and send {"type":"rescue"}.
  2. Start it. The rescue system takes a minute or two to come up; retry SSH until it answers. Log in as root at the VM's address, with the VM's own SSH keys.
  3. Mount its disk and fix what locked you out. The VM's disk is /dev/sda, not mounted, and attached volumes are further disks. Mount its root with mount /dev/disk/by-label/cloudimg-rootfs /mnt, and unmount it before you stop the VM.
  4. Stop it, send {"type":"unrescue"}, and start it.

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 state of the VM's record on our side (creating, active, retained or deleting), and power is read live from the hypervisor on every request — running, stopped, or unknown when the hypervisor could not be reached, in which case health is unreachable. 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 errors come back before the build starts: 409 no_capacity_available when there is nowhere to place a VM right now (not a quota, so raising yours does not help), 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 counting against your 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. GET /v1/vms/{id} reports power live from the hypervisor whenever you want to look. stop shuts the guest down cleanly and forces it off after about 30 seconds (pass "force": true to cut power at once). reboot asks the guest twice and never forces it: one that ignores both stays running, and the error says so. start and stop are both idempotent, so a machine already in the state you asked for answers 200 rather than an error. A start can be refused with 409 no_capacity_available when there is no room to run that much RAM right now: the VM stays stopped and nothing else changed. It is not a quota; retry later. A stopped VM keeps no room for itself, so this can follow any stop, including one for a resize, a rollback or rescue. Stopping a VM stops its RAM charge and nothing else: the root disk, the address, its snapshots and its volumes bill whether it runs or not, so delete a machine you no longer need instead of leaving it stopped. GET /v1/vms/{id}/events records what happened to a VM, newest first — it is where to look when an action did not do what you expected. A failed create leaves no VM to ask about: its 409 create_failed is the whole record. 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, unless retention keeps it. Nothing else is deleted for you: 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.

Retention

GET /v1/retention                     {"days": 0, ...}
PUT /v1/retention   {"days": 7}       0 to 30, for the whole project

With days above 0, deleting a VM or a volume keeps it for that many days instead of destroying it. The DELETE answers retained in its status (state for a volume) with a purge_at time, and the resource still lists that way. At 0, the default, a delete destroys at once. **Snapshots are never kept**: deleting one destroys it whatever days says.

``` POST /v1/vms/{id}/actions {"type":"undelete"} POST /v1/volumes/{id}/actions {"type":"undelete"} ```

After purge_at it answers 409 retention_expired, and on something you have not deleted, 409 not_retained.

Images

Stock images only; no custom image upload. GET /v1/images lists 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.

Anything custom happens in-guest after boot.

Keep a setup manifest

No image captures a configured machine, so for each VM keep a manifest you can re-run, stored somewhere other than the VM:

Update it whenever you change the machine. With it, a replacement is one create and one run: Recreate a VM.