> ## Documentation Index
> Fetch the complete documentation index at: https://docs.basaltic.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Launching an instance

> Flavors, images, disks, keys and cloud-init — and the two fields whose absence quietly costs you a network.

Client examples use CLI v0.13.0 and Go SDK v0.15.0. See [CLI setup](/cli) and
[Go configuration](/reference-resolution#released-go-sdk). Go snippets assume a configured `cfg`, a
`ctx` from `context.Background()`, and imports for `log`, `basaltic`
(`github.com/basaltic-sh/sdk-go`) and `compute`
(`github.com/basaltic-sh/sdk-go/compute`).

<Tabs>
  <Tab title="Console">
    Go to **Compute → Instances** and choose **Create instance**. The form is
    one card per decision — **Details**, **Flavor**, **Image**, **Boot
    volume**, **Data volumes**, **Networking**, **SSH keys**, **IAM role**,
    **User data** — with a running summary alongside them.

    **Networking** is the card to slow down on. Every interface carries its own
    **VPC**, **Subnet**, **Security groups** and **Public floating IPs**;
    **Add network interface** adds another, and the first in the list is the
    primary NIC.
  </Tab>

  <Tab title="API">
    Replace the example identities with resources in your account. This request
    mixes a flavor UUID, a current image name, a keypair name and a nested subnet CRN.

    ```bash theme={null}
    POST https://compute.sa-saopaulo-1.basaltic.sh/v1/instances
    {
      "name": "web-01",
      "flavor": "550e8400-e29b-41d4-a716-446655440000",
      "image": "debian-13",
      "keypairs": ["my-keypair"],
      "networks": [
        { "subnet": "crn:network:sa-saopaulo-1:my-account:vpc/production/subnet/private",
          "security_groups": ["c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f"],
          "floating_ip_assignment": "ipv4" }
      ]
    }
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    basaltic compute instance create --name web-01 \
      --flavor 550e8400-e29b-41d4-a716-446655440000 --image debian-13 \
      --keypairs my-keypair \
      --networks '[{"subnet":"crn:network:sa-saopaulo-1:my-account:vpc/production/subnet/private","security_groups":["c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f"],"floating_ip_assignment":"ipv4"}]'
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    _, err := compute.New(cfg).CreateInstance(ctx, &compute.InstanceCreateRequest{
        Name: "web-01", Flavor: "550e8400-e29b-41d4-a716-446655440000",
        Image: basaltic.String("debian-13"), Keypairs: []string{"my-keypair"},
        Networks: []*compute.NetworkConfig{{
            Subnet: "crn:network:sa-saopaulo-1:my-account:vpc/production/subnet/private",
            SecurityGroups: []string{"c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f"},
            FloatingIPAssignment: basaltic.String("ipv4"),
        }},
    })
    if err != nil {
        log.Fatal(err)
    }
    ```
  </Tab>
</Tabs>

The call answers **`202`** with the instance in `pending`. Building the boot
disk, the interfaces and the seed happens after the response, so poll
`GET /v1/instances/{instance_id}` and watch `current_state`.

<Warning>
  **Security groups are per interface.** They belong in
  `networks[].security_groups`, not in a top-level field. A NIC provisioned
  with an empty list gets no per-interface filtering at all, which is not the
  same thing as a closed instance.
</Warning>

<Warning>
  **An instance with no `networks` entry boots with no networking.** Nothing
  fails and nothing warns you — the guest comes up with no interface, so
  nothing reaches it and it reaches nothing. Send at least one entry; index 0
  becomes the primary NIC, the one carrying the guest's default route and its
  route to the metadata service.
</Warning>

## Sizing: flavors

A flavor is CPU and memory, and nothing else. It carries **no disk size** —
the boot disk is a volume sized at launch — so `GET /v1/flavors` is a catalog
of compute shapes, not of machine types.

<ResponseField name="class" type="shared | dedicated">
  `shared` oversubscribes CPU for higher density. `dedicated` pins each vCPU
  1:1 to a physical core. Both classes run on the same hosts; the class decides
  how the instance draws on a host's threads.
</ResponseField>

<ResponseField name="family" type="general | loadbalancer | database">
  Only `general` flavors can run instances and instance pools. The other two
  are reserved for the managed products, whose nodes are operated and priced
  by the platform, and are refused here. Pass `?family=general` when listing.
</ResponseField>

<ResponseField name="cpu_baseline_pct / cpu_burst_pct" type="percent of one vCPU">
  The guaranteed floor and the ceiling. The instance is entitled to
  `vcpus × cpu_baseline_pct / 100` cores however busy its neighbours get, and
  may not exceed the burst ceiling even on an idle host. Absent means the
  flavor guarantees no floor, or imposes no ceiling beyond the vCPU count.
</ResponseField>

<ResponseField name="net_mbps" type="aggregate throughput">
  The instance's network ceiling, in megabits/s, across all its interfaces.
  Absent means uncapped. A [resize](#resize) moves this with the flavor.
</ResponseField>

The catalog is small and comes back in one page — `GET /v1/flavors` is not
paginated.

## Choosing an image

`image` takes four forms, and the difference matters for reproducibility:

<Tabs>
  <Tab title="A bare name">
    `"image": "debian-13"` follows the tag. You get whichever build is
    current at the moment the instance is created, so two launches a month
    apart can boot different bits.
  </Tab>

  <Tab title="name:version">
    `"image": "debian-13:20260807"` pins one build. This is how you opt out
    of the tag moving under you.
  </Tab>

  <Tab title="A CRN">
    `crn:compute:sa-saopaulo-1:platform:image/debian-13/architecture/amd64/version/20260807`
    pins the owner, name, architecture and version.
  </Tab>

  <Tab title="An image id">
    A UUID pins one build too, and is what the API returns to you.
  </Tab>
</Tabs>

Names resolve across your own images plus the public platform catalog, and
use the request architecture (default `amd64`). A complete image CRN pins its architecture. See [Images and keypairs](/compute/images)
for how a tag moves and what the listing does and does not show you.

## Disks

`volumes` is one list, and the boot disk is the entry that says so:

```json theme={null}
"volumes": [
  { "boot": true, "size_gb": 40, "volume_type": "nvme" },
  { "size_gb": 100, "mount_path": "/data", "fstype": "ext4" }
]
```

<ResponseField name="boot" type="boolean">
  Marks the disk cloned from `image`. **At most one entry may set it**, and
  a boot entry takes no `mount_path` or `fstype` — both come from the image,
  so sending either is refused rather than ignored.

  Omit the boot entry entirely and you get the image's `min_disk_gb` on the
  region's default tier.
</ResponseField>

<ResponseField name="size_gb" type="integer, 1–16384" required>
  On the boot entry this may not go below the image's `min_disk_gb`. Anything
  smaller is a `400` before the instance row exists, not a failed build.
</ResponseField>

<ResponseField name="mount_path" type="string">
  With a mount path set, the in-guest agent formats the disk — only if it is
  blank — and mounts it there, using `fstype`: `ext4` by default, or `xfs`.
</ResponseField>

<ResponseField name="delete_on_termination" type="boolean, default true">
  Every launch-time volume defaults to true, which is the opposite of a volume
  you attach later.
</ResponseField>

<Warning>
  **The boot volume is destroyed when the instance is deleted.** If you want it
  to outlive the instance, flip `delete_on_termination` on the attachment after
  launch — see [what survives a delete](/compute/lifecycle#what-survives-a-delete).
</Warning>

## SSH keys and cloud-init

`keypairs` authorizes existing keypairs on the instance. The public keys are
**baked into the seed at launch**, so deleting the keypair afterwards does not
remove access from an instance already running.

`user_data` is base64-encoded cloud-init, and the platform merges it over a
base configuration that creates the login user:

|                 |                                                |
| --------------- | ---------------------------------------------- |
| Default user    | `basaltic`, in `sudo`, with `NOPASSWD:ALL`     |
| Authorized keys | the public keys of every keypair in `keypairs` |
| Password login  | disabled (`ssh_pwauth: false`)                 |
| Root login      | disabled                                       |

<Warning>
  **Your user-data must decode to a document whose first line is
  `#cloud-config`.** Anything else — a `#!/bin/bash` script, a MIME multipart
  bundle — is ignored in silence: the instance boots, your script never runs,
  and nothing in the API says so. Wrap shell work in cloud-init's `runcmd`
  instead.
</Warning>

<Warning>
  **The merge is by top-level key, not a deep merge.** A key you set replaces
  the base entirely. Sending your own `users:` block therefore replaces the
  `basaltic` user — and takes your keypair's authorized keys with it, because
  those live inside it. Add users under a different key, or restate the base
  user's `ssh_authorized_keys` yourself.
</Warning>

A `user_data` document that is not valid YAML fails the build, and the
instance lands in `error` rather than booting without it.

## Giving the instance an identity

At launch, `iam_role` accepts a role name, UUID or CRN and attaches that IAM role. Software inside the guest then fetches
short-lived credentials from the metadata service at `169.254.169.254` — no
access key is written to the instance, and nothing has to be rotated.

<Note>
  Two conditions, and both are easy to miss. The role's **trust policy** must
  accept `crn:compute:*:*:instance/*` (or the specific instance CRN), and the
  principal making the launch call needs **`iam:PassRole`** on the role,
  separately from `compute:CreateInstance`. Without the trust policy the
  instance launches and the credentials never mint; without `iam:PassRole` the
  launch itself is denied. [Roles and instance identity](/iam/roles) walks
  through both.
</Note>

Instance responses return `iam_role` as an optional summary instead of a string
reference or `iam_role_id`:

```json theme={null}
{
  "iam_role": {
    "id": "b2c3d4e5-f6a7-8901-2345-67890abcdef1",
    "crn": "crn:iam::my-account:role/deploy",
    "name": "deploy"
  }
}
```

The summary is visible with instance read access; it does not require
`iam:GetRole`. It contains only `id`, `crn` and `name`, excluding sensitive role
fields such as policies. The field is omitted when no role is attached, the
role has been deleted or it belongs to another account. Opening the role's
IAM details still requires `iam:GetRole`.

## Names, tags and metadata

<ResponseField name="name" type="unique per account">
  1–128 characters, DNS-safe: letters, digits, dot, dash, underscore, starting
  and ending alphanumeric. A name that collides with another instance in the
  account is a `409`.
</ResponseField>

<ResponseField name="renaming" type="not supported">
  `PATCH /v1/instances/{instance_id}` edits `description`, `metadata` and
  `tags`. A name is fixed for the life of the instance.

  In the console those three are the **Details**, **Tags** and **Metadata**
  cards on the instance's **Settings** tab, and there is no name field among
  them.
</ResponseField>

<ResponseField name="tags" type="IAM and cost">
  Read by IAM policy conditions as `basalt:RequestTag/<key>` on the create and
  `basalt:ResourceTag/<key>` afterwards, and used for cost attribution. See
  [policies](/iam/policies).
</ResponseField>

<ResponseField name="metadata" type="replaced, not merged">
  The map you `PATCH` becomes the whole set of your keys.
</ResponseField>

<Note>
  Metadata keys under the `basalt:` namespace are control-plane state — how an
  instance proves which pool or managed resource it belongs to. You cannot set
  them, and you cannot remove them: they are preserved through a `PATCH`
  whether or not you echo them back, and proposing a *different* value for one
  is a `400` rather than a silent drop. Read-modify-write against the whole
  metadata map is therefore safe.
</Note>

## Reading instance addresses

The instances table shows **Private IPv4**, **Public IPv4**, and **IPv6** from
the primary NIC. Instance details show those same values individually. The
**Networking** tab lists each NIC with its primary addresses. An attached
IPv6 floating IP is shown instead of the NIC's directly attached IPv6 address.

The overview shows one address per instance, preferring public IPv4, then
public IPv6, then private IPv4.

## Resource references

See [Resource references](/reference-resolution) for classification, image versions
and fetching an instance by name or CRN.

`flavor`, `keypairs`, `iam_role`, and per-NIC `security_groups` accept a UUID,
a CRN, or an exact name. Flavors are regional; keypairs and security groups
belong to your account, and IAM roles belong to your account.
`subnet` accepts a UUID or a complete VPC/subnet CRN. A bare subnet name cannot
be resolved without its VPC parent. Interface attachment likewise requires
a UUID or complete VPC/subnet/interface CRN.

Every compute list accepts exact `name` and `crn` filters. They intersect with
other filters and pagination. A foreign or mismatched CRN matches no rows;
a malformed or empty CRN returns 400. Empty names do not broaden the list.
Instance lists also accept `flavor` and `image` references. NIC-list names
match interface names within the instance bindings; duplicate names can return
multiple interfaces. Floating IPs have no
name identity.
