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.
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.
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; 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.
The usual setup: a database on the root disk, its backups on a volume.
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.
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}
stopped (409 vm_not_stopped): unmount in-guest, stop, detach.409 volume_in_snapshots and names those snapshots in the error's
snapshots field; delete them first. A snapshot can restore a volume only
while the volume is still on the VM that took it, so moving a volume means
giving up the snapshots that include it. Snapshots taken before you attached
it don't count.size_gb must be
a multiple of 250 on create and on grow.growpart + resize2fs for ext4, xfs_growfs for xfs). No stop, no
detach.409 must_detach_first), and it
destroys the data, unless retention keeps it.409 vm_slots_full. If you need more, add a VM.409 volume_moved
says it was attached, detached or deleted between your read and your write:
re-read GET /v1/volumes/{id} and decide again, because retrying the same
request will not fix it. 409 volume_already_grown says another resize
passed the size you asked for; retry only with a larger number. Neither
applied anything.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.
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
GET /v1/billing/prices).
Billing is per snapshot, so GET /v1/usage?group_by=resource tells you
which one to delete. Deleting it stops the charge within the minute.POST /v1/vms/{id}/snapshot) cover the root **and all
attached volumes at once. They're crash-consistent** — the same thing
your filesystem sees after a power cut, which ext4/xfs and any crash-safe
database recover from cleanly. Memory isn't captured, so after a rollback
the VM boots fresh; running programs are not restored.409 snapshot_volumes_changed naming it.
Detach it, or snapshot again and roll back to that. The volumes a snapshot
includes are still there: they cannot be detached until it is deleted.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.
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:
:25 is blocked — at the network, upstream of your VM, so
no in-guest configuration changes it. You cannot run your own mail server
that delivers to receiving servers directly; send through the
relay instead. Submission ports (587, 465) outbound to
third parties are not blocked.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.
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:
{"type":"rescue"}.root at the VM's address, with the VM's
own SSH keys./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.{"type":"unrescue"}, and start it.-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null, so the key
you saved for the VM is still there when it boots its own disk again.409 rescue_unavailable means rescue cannot be used on this VM; open a
support ticket.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.
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.
purge_at it bills and counts against your quota as before. A VM
is stopped and keeps its IP; a volume stays detached.``` 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.
409 vm_retained or
volume_retained. Deleting it again changes nothing; a delete after
purge_at destroys it at once.days applies to deletes made after it; anything already retained
keeps its purge_at.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.
No image captures a configured machine, so for each VM keep a manifest you can re-run, stored somewhere other than the VM:
ram_mb, disk_gb, the pinned img_... id, the SSH
keysUpdate it whenever you change the machine. With it, a replacement is one create and one run: Recreate a VM.