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

# Customize the initial setup for a Virtual Machine

export const MethodSection = ({children}) => children ?? null;

export const MethodSwitch = ({children}) => {
  const tabs = React.Children.toArray(children).map(c => {
    if (!c || !c.props) return null;
    if (c.props.id) return c;
    const inner = c.props.children;
    if (inner && inner.props && inner.props.id) return inner;
    return null;
  }).filter(Boolean);
  const firstId = tabs.length > 0 ? tabs[0].props.id : "";
  const [active, setActive] = React.useState(firstId);
  React.useEffect(() => {
    try {
      const saved = localStorage.getItem("gcore_docs_method");
      if (saved && tabs.find(t => t.props.id === saved)) {
        setActive(saved);
      }
    } catch (_) {}
  }, []);
  React.useEffect(() => {
    try {
      document.querySelectorAll("h2[id], h3[id]").forEach(heading => {
        const visible = heading.offsetParent !== null;
        document.querySelectorAll(`a[href="#${heading.id}"]`).forEach(link => {
          if (link.closest("h1,h2,h3,h4,h5,h6")) return;
          const li = link.closest("li");
          if (li) li.style.display = visible ? "" : "none";
        });
      });
    } catch (_) {}
    window.dispatchEvent(new Event("scroll"));
  }, [active]);
  const handleClick = id => {
    setActive(id);
    try {
      localStorage.setItem("gcore_docs_method", id);
    } catch (_) {}
  };
  return <div>
      <div className="not-prose flex gap-0 border-b border-zinc-200 dark:border-zinc-800 mb-8 mt-2" role="tablist">
        {tabs.map(tab => {
    const isActive = active === tab.props.id;
    return <button key={tab.props.id} role="tab" aria-selected={isActive} onClick={() => handleClick(tab.props.id)} className={["px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors cursor-pointer", isActive ? "border-primary text-primary" : "border-transparent text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-200"].join(" ")}>
              {tab.props.label}
            </button>;
  })}
      </div>

      {tabs.map(tab => <div key={tab.props.id} style={{
    display: active === tab.props.id ? "" : "none"
  }}>
          {tab.props.children}
        </div>)}
    </div>;
};

<MethodSwitch>
  <MethodSection id="portal" label="Customer Portal">
    <p>When [creating](/cloud/virtual-instances/create-an-instance) a Virtual Machine, the **Additional options** section includes a **User data** field that passes a `cloud-init` YAML script to the VM on first boot. Use it to set passwords, create users, install packages, write files, and configure network interfaces on Linux-based Virtual Machines. Windows Virtual Machines do not support `cloud-init`.</p>

    ## Enable User data

    <p>By default, the **User data** field is disabled for new Virtual Machines. Enable it by switching on the **User data** toggle. The portal pre-fills a sample password configuration template — edit the template or replace it with a custom cloud-init YAML document.</p>

    <Frame>
      <img src="https://mintcdn.com/gcore/yrEa6OGy4Q_zWlXb/images/docs/cloud/virtual-instances/customize-initial-setup-for-your-instance/user-data-enabled.png?fit=max&auto=format&n=yrEa6OGy4Q_zWlXb&q=85&s=29c135994728d588b031bfa276914d0b" alt="User data field enabled with default cloud-init template" width="723" height="600" data-path="images/docs/cloud/virtual-instances/customize-initial-setup-for-your-instance/user-data-enabled.png" />
    </Frame>
  </MethodSection>

  <MethodSection id="api" label="REST API">
    <p>The `user_data` field in the [instance creation](/api-reference/cloud#tag/Instances/operation/InstanceCreateSetV2.post) request passes a `cloud-init` YAML script to the VM on first boot. The value must be base64-encoded — the API rejects plain-text YAML.</p>

    <Info>
      An [API token](/account-settings/api-tokens) is required, along with a [project ID](/api-reference/cloud/projects/list-projects) and [region ID](/api-reference/cloud/regions/list-regions).
    </Info>

    <p>Open a terminal and set these as environment variables before running the examples:</p>

    ```bash theme={null}
    export GCORE_API_KEY="{YOUR_API_KEY}"
    export GCORE_CLOUD_PROJECT_ID="{YOUR_PROJECT_ID}"
    export GCORE_CLOUD_REGION_ID="{YOUR_REGION_ID}"
    export IMAGE_ID="{YOUR_IMAGE_ID}"
    export FLAVOR_ID="{YOUR_FLAVOR_ID}"
    ```

    <Info>
      To find the values for each variable:

      * **IMAGE\_ID**: the UUID of a public or private image. Use `GET /cloud/v1/images/{project_id}/{region_id}` to list available images; the [image upload](/cloud/images/upload-an-image-to-the-storage) article covers custom uploads.
      * **FLAVOR\_ID**: the flavor identifier for the VM size. Use `GET /cloud/v1/flavors/{project_id}/{region_id}` to list flavors available in the region.
    </Info>

    ## Quickstart

    <p>The scripts below base64-encode a cloud-init YAML document that writes a sentinel file to `/tmp`, create a VM with that script, and print the instance ID.</p>

    <Tabs>
      <Tab title="Python SDK">
        ```python theme={null}
        import base64
        import os
        from gcore import Gcore

        client = Gcore()

        cloud_init_yaml = """\
        #cloud-config
        write_files:
        - path: /tmp/cloud-init-ok.txt
        content: |
          cloud-init ran successfully
        permissions: '0644'
        """

        user_data_b64 = base64.b64encode(cloud_init_yaml.encode()).decode()

        instance = client.cloud.instances.create_and_poll(
            name="my-vm",
            flavor=os.environ["FLAVOR_ID"],
            user_data=user_data_b64,
            interfaces=[{"type": "external"}],
            volumes=[{
                "source": "image",
                "image_id": os.environ["IMAGE_ID"],
                "size": 20,
                "boot_index": 0,
            }],
        )
        print(f"Instance: {instance.id}  status: {instance.status}")
        ```
      </Tab>

      <Tab title="Go SDK">
        ```go theme={null}
        package main

        import (
            "context"
            "encoding/base64"
            "fmt"
            "log"
            "os"

            gcore "github.com/G-Core/gcore-go"
            "github.com/G-Core/gcore-go/cloud"
        )

        func main() {
            client := gcore.NewClient()
            ctx := context.Background()

            cloudInitYAML := "#cloud-config\n" +
                "write_files:\n" +
                "  - path: /tmp/cloud-init-ok.txt\n" +
                "    content: |\n" +
                "      cloud-init ran successfully\n" +
                "    permissions: '0644'\n"

            userDataB64 := base64.StdEncoding.EncodeToString([]byte(cloudInitYAML))

            instance, err := client.Cloud.Instances.NewAndPoll(ctx, cloud.InstanceNewParams{
                Flavor:   os.Getenv("FLAVOR_ID"),
                Name:     gcore.String("my-vm"),
                UserData: gcore.String(userDataB64),
                Interfaces: []cloud.InstanceNewParamsInterfaceUnion{{
                    OfExternal: &cloud.InstanceNewParamsInterfaceExternal{},
                }},
                Volumes: []cloud.InstanceNewParamsVolumeUnion{{
                    OfImage: &cloud.InstanceNewParamsVolumeImage{
                        ImageID:   os.Getenv("IMAGE_ID"),
                        Size:      gcore.Int(20),
                        BootIndex: gcore.Int(0),
                    },
                }},
            })
            if err != nil {
                log.Fatalf("create instance: %v", err)
            }
            fmt.Printf("Instance: %s  status: %s\n", instance.ID, instance.Status)
        }
        ```
      </Tab>
    </Tabs>

    ## Step-by-step

    <p>Each step explains what the call does, which parameters matter, and what the response looks like.</p>

    <Accordion title="Show all steps">
      ### Step 1. Write the cloud-init YAML

      Every cloud-init document must begin with `#cloud-config` as its first line. The example below writes a file to `/tmp` to confirm initialization ran:

      ```yaml theme={null}
      #cloud-config
      write_files:
        - path: /tmp/cloud-init-ok.txt
          content: |
            cloud-init ran successfully
          permissions: '0644'
      ```

      Save this file as `user-data.yaml`.

      ### Step 2. Encode to base64

      The `user_data` field requires the YAML encoded as a single-line base64 string.

      <Tabs>
        <Tab title="Python SDK">
          ```python theme={null}
          import base64

          with open("user-data.yaml", "rb") as f:
              user_data_b64 = base64.b64encode(f.read()).decode()
          print(user_data_b64)
          ```
        </Tab>

        <Tab title="Go SDK">
          ```go theme={null}
          import (
              "encoding/base64"
              "fmt"
              "os"
          )

          data, _ := os.ReadFile("user-data.yaml")
          userDataB64 := base64.StdEncoding.EncodeToString(data)
          fmt.Println(userDataB64)
          ```
        </Tab>

        <Tab title="curl">
          ```bash theme={null}
          USER_DATA_B64=$(base64 -w 0 user-data.yaml)
          echo "$USER_DATA_B64"
          ```

          The `-w 0` flag disables line-wrapping. On macOS, use `base64 -i user-data.yaml` instead.
        </Tab>
      </Tabs>

      ### Step 3. Create the instance

      Pass the encoded string in the `user_data` field of the instance creation request.

      | Parameter    | Required | Description                                                                                        |
      | ------------ | -------- | -------------------------------------------------------------------------------------------------- |
      | `user_data`  | No       | Base64-encoded cloud-init YAML; runs on first boot only.                                           |
      | `flavor`     | Yes      | [Flavor](/api-reference/cloud#tag/Flavors/operation/FlavorListV2.get) ID; IDs are region-specific. |
      | `boot_index` | Yes      | Set to `0` for the primary boot volume.                                                            |

      <Tabs>
        <Tab title="Python SDK">
          ```python theme={null}
          import base64
          import os
          from gcore import Gcore

          client = Gcore()

          with open("user-data.yaml", "rb") as f:
              user_data_b64 = base64.b64encode(f.read()).decode()

          instance = client.cloud.instances.create_and_poll(
              name="my-vm",
              flavor=os.environ["FLAVOR_ID"],
              user_data=user_data_b64,
              interfaces=[{"type": "external"}],
              volumes=[{
                  "source": "image",
                  "image_id": os.environ["IMAGE_ID"],
                  "size": 20,
                  "boot_index": 0,
              }],
          )
          print(f"Instance: {instance.id}  status: {instance.status}")
          ```
        </Tab>

        <Tab title="Go SDK">
          ```go theme={null}
          package main

          import (
              "context"
              "encoding/base64"
              "fmt"
              "log"
              "os"

              gcore "github.com/G-Core/gcore-go"
              "github.com/G-Core/gcore-go/cloud"
          )

          func main() {
              client := gcore.NewClient()
              ctx := context.Background()

              data, _ := os.ReadFile("user-data.yaml")
              userDataB64 := base64.StdEncoding.EncodeToString(data)

              instance, err := client.Cloud.Instances.NewAndPoll(ctx, cloud.InstanceNewParams{
                  Flavor:   os.Getenv("FLAVOR_ID"),
                  Name:     gcore.String("my-vm"),
                  UserData: gcore.String(userDataB64),
                  Interfaces: []cloud.InstanceNewParamsInterfaceUnion{{
                      OfExternal: &cloud.InstanceNewParamsInterfaceExternal{},
                  }},
                  Volumes: []cloud.InstanceNewParamsVolumeUnion{{
                      OfImage: &cloud.InstanceNewParamsVolumeImage{
                          ImageID:   os.Getenv("IMAGE_ID"),
                          Size:      gcore.Int(20),
                          BootIndex: gcore.Int(0),
                      },
                  }},
              })
              if err != nil {
                  log.Fatalf("create instance: %v", err)
              }
              fmt.Printf("Instance: %s  status: %s\n", instance.ID, instance.Status)
          }
          ```
        </Tab>

        <Tab title="curl">
          ```bash theme={null}
          curl -X POST "https://api.gcore.com/cloud/v2/instances/$GCORE_CLOUD_PROJECT_ID/$GCORE_CLOUD_REGION_ID" \
            -H "Authorization: APIKey $GCORE_API_KEY" \
            -H "Content-Type: application/json" \
            -d '{
              "name": "my-vm",
              "flavor": "'"$FLAVOR_ID"'",
              "user_data": "'"$USER_DATA_B64"'",
              "interfaces": [{"type": "external"}],
              "volumes": [{
                "source": "image",
                "image_id": "'"$IMAGE_ID"'",
                "size": 20,
                "boot_index": 0
              }]
            }'
          ```

          The API returns:

          ```json theme={null}
          {
            "tasks": ["b63f0fff-5d22-4c23-8e09-f7a3e1c1a999"]
          }
          ```

          Poll `GET /cloud/v1/tasks/{task_id}` every 5 seconds until `state` is `FINISHED`, then read the instance ID from `created_resources.instances[0]`.
        </Tab>
      </Tabs>

      ### Step 4. Verify cloud-init ran

      After the instance is active, connect via SSH and check the sentinel file:

      ```bash theme={null}
      ssh ubuntu@{INSTANCE_IP} cat /tmp/cloud-init-ok.txt
      ```

      Expected output:

      ```text theme={null}
      cloud-init ran successfully
      ```

      If the file is absent, check `/var/log/cloud-init.log` and `/var/log/cloud-init-output.log` on the VM.
    </Accordion>
  </MethodSection>

  <MethodSection id="terraform" label="Terraform">
    <p>Automate Virtual Machine setup on first boot by passing a cloud-init YAML document through `user_data` on [`gcore_cloud_instance`](https://registry.terraform.io/providers/G-Core/gcore/latest/docs/resources/cloud_instance). No Terraform-specific syntax required — standard [cloud-init directives](https://cloudinit.readthedocs.io/en/latest/reference/modules.html) apply.</p>

    <Warning>
      `user_data` runs on first boot only — changes to an existing configuration take effect only when the instance is recreated.
    </Warning>

    ```hcl theme={null}
    resource "gcore_cloud_instance" "example" {
      project_id   = var.project_id
      region_id    = var.region_id
      flavor       = "g2-standard-2-4"
      name         = "my-vm"
      ssh_key_name = "my-keypair"

      volumes    = [{ volume_id = gcore_cloud_volume.boot.id }]
      interfaces = [{ type = "external", ip_family = "ipv4" }]

      user_data = base64encode(<<-EOF
        #cloud-config
        password: your-password
        chpasswd: { expire: false }
        ssh_pwauth: true
      EOF
      )
    }
    ```

    <p>The [Virtual Machine](/cloud/virtual-instances/create-an-instance) article covers the full configuration including boot volume and SSH key.</p>
  </MethodSection>
</MethodSwitch>

### Set a password

By default, password-based SSH authentication is disabled on Gcore Virtual Machines — only SSH key authentication is enabled. Set `ssh_pwauth: true` to allow password access.

```yaml theme={null}
#cloud-config
password: your_password
chpasswd: { expire: false }
ssh_pwauth: true
power_state:
  mode: reboot
  timeout: 30
  condition: true
```

* `password` — sets the password for the default OS user (e.g. `ubuntu` on Ubuntu images).
* `chpasswd: { expire: false }` — keeps the password active without requiring a change on first login.
* `ssh_pwauth: true` — enables password-based SSH authentication.
* `power_state` — reboots the VM after cloud-init finishes so the changes take effect.

After the VM is ready, connect with:

```bash theme={null}
ssh your_default_username@your_instance_ip_address
```

### Create a user

Add a `users` block to create a new OS user at boot. The example below creates a `guest` user with sudo access.

```yaml theme={null}
#cloud-config
users:
  - name:         guest
    sudo:         ALL=(ALL) NOPASSWD:ALL
    passwd:       SHA-512_encrypted_value_of_guest_password
    groups:       users,admin
    lock_passwd:  false
    shell:        /bin/bash
power_state:
  mode: reboot
  timeout: 30
  condition: true
```

* `sudo: ALL=(ALL) NOPASSWD:ALL` — grants the user passwordless sudo rights.
* `passwd` — SHA-512 encrypted password. Generate it with `mkpasswd -m sha512crypt guest`.
* `groups` — comma-separated list of groups. The groups must already exist on the system.
* `lock_passwd: false` — allows password-based login.
* `shell: /bin/bash` — sets the default shell.

<Info>
  Run `groups` after connecting to verify group membership.
</Info>

### Enable root user

Set `disable_root: false` to allow the root user to log in with SSH.

```yaml theme={null}
#cloud-config
disable_root: false
```

### Configure user groups

Add the `groups` directive to create custom groups at boot.

```yaml theme={null}
#cloud-config
groups:
  - regular-users
power_state:
  mode: reboot
  timeout: 30
  condition: true
```

After the VM is ready, run `compgen -g` to verify the group appears.

### Add an SSH key

Add public SSH keys from other machines using `ssh_authorized_keys`.

```yaml theme={null}
#cloud-config
ssh_authorized_keys:
  - ssh-rsa <YOUR_PUBLIC_KEY> user@host
```

After the VM starts, verify with `cat ~/.ssh/authorized_keys`.

### Add repositories and install packages

Install packages at first boot using the `packages` directive. Packages from external repositories require an `apt.sources` entry.

<Info>
  The example below uses `apt` and applies to Debian-based distributions (Ubuntu, Debian). For RPM-based distributions (CentOS, Rocky Linux, Fedora), use the `yum_repos` directive and replace `packages` entries with the corresponding package names.
</Info>

```yaml theme={null}
#cloud-config
apt:
  sources:
    docker.list:
      source: deb [arch=amd64] https://download.docker.com/linux/ubuntu $RELEASE stable
      keyid: 9DC858229FC7DD38854AE2D88D81803C0EBFCD88
package_update: true
package_upgrade: true
packages:
  - apt-transport-https
  - ca-certificates
  - gnupg-agent
  - software-properties-common
  - gnupg
  - docker-ce
  - docker-ce-cli
  - nmap
  - traceroute
power_state:
  mode: reboot
  timeout: 30
  condition: true
```

### Write files

Create files on the VM at boot using the `write_files` directive.

```yaml theme={null}
#cloud-config
write_files:
  - path: /root/useful-docker-script.sh
    permissions: '0644'
    content: |
      #!/bin/bash
      docker build -t dockerhub-username/docker-image-name .
      docker login
      docker push dockerhub-username/docker-image-name
```

### Configure network interfaces

Add a static IP address by writing a Netplan configuration file. Cloud-init merges it with the auto-generated `50-cloud-init.yaml` during boot.

<Info>
  Netplan applies to Ubuntu. For other distributions, write the appropriate network configuration file for the system's network manager (for example, NetworkManager connection files on Rocky Linux or CentOS).
</Info>

```yaml theme={null}
#cloud-config
write_files:
  - content: |
      network:
          version: 2
          renderer: networkd
          ethernets:
            enp3s0:
              addresses:
              - 192.170.1.25/24
              - 2020:1::1/64
              nameservers:
                addresses:
                - 8.8.8.8
                - 8.8.4.4
    path: /etc/netplan/00-add-static-ip.yaml
    permissions: '0644'
power_state:
  mode: reboot
  timeout: 30
  condition: true
```

After the VM reboots, run `ip a` to verify the static addresses appear on the `enp3s0` interface.

## Check cloud-init logs

Two log files are available on the VM for diagnosing cloud-init issues.

**`/var/log/cloud-init.log`** — full process log with debug output:

```bash theme={null}
cat /var/log/cloud-init.log
```

**`/var/log/cloud-init-output.log`** — stdout/stderr from scripts cloud-init ran:

```bash theme={null}
cat /var/log/cloud-init-output.log
```
