VPS.TC
| $
Server Status
Turkey Istanbul, Türkiye
Active
USA New York, USA
Active
Cart Total:
View Cart
What Is Cloud-Init? Automate VPS Provisioning
Cloud Computing

What Is Cloud-Init? Automate VPS Provisioning

Avatar of Defne Defne September 1, 2026 16 min read 0 Comments
Share:

The first boot is where cloud-init earns its keep

A new VPS rarely begins with one big task. It begins with fifteen small ones: update the package index, create an administration user, install an SSH key, set the timezone, prepare swap, and install the few services you need. Doing that once is fine. Doing it for the tenth VPS is how a missed option or a copied command turns into an incident.

Cloud-init handles this first layer. It is a service that applies operating system and user configuration when a cloud server boots for the first time. Cloud-init reads metadata supplied by your provider and the user-data you provide, then configures things such as the hostname, users, SSH keys, network settings, packages, and first-boot commands.

🚀 Boost Your Speed with VPS Server!

Speed up your projects with high-performance SSD storage and 99.9% uptime guarantee.

Get Started

There is one detail worth keeping clear: cloud-init usually does not install the operating system. The provider’s cloud image has already been written to disk and prepared for its first boot. Cloud-init turns that prepared image into the particular server you requested.

When I first used it, I thought cloud-init was basically a small Ansible file. I was wrong. Cloud-init runs while the machine is completing its first boot; Ansible normally connects later, once SSH is available. They solve related problems, but they start from different places.

What happens during first boot?

The exact ordering varies with the distribution, cloud-init release, and datasource, but the service works through several stages. Knowing the stages makes questions such as “why was the user not created?” much easier to investigate.

☁️ Gain Flexibility with Cloud Server!

Experience the power of cloud with scalable resources and instant backups.

Explore
  • Local stage: The system decides whether cloud-init should run and looks for a local datasource.
  • Network stage: After the network is available, cloud-init can retrieve metadata and user-data from a datasource that requires networking.
  • Config stage: Modules configure users, SSH keys, package repositories, the hostname, and files.
  • Final stage: Commands such as runcmd are executed. If package installation is still running, this stage may finish later than you expect.

On an Ubuntu cloud image, I usually inspect the related units with:

systemctl status cloud-init-local.service
systemctl status cloud-init.service
systemctl status cloud-config.service
systemctl status cloud-final.service

The last unit is particularly useful. On the Ubuntu images I run, runcmd commands generally execute during cloud-final.service. In one test I connected over SSH as soon as the connection became available and assumed the VPS was ready. Package installation was still in progress, so the commands I ran afterwards failed in confusing ways.

SSH being available is not the same as first boot being finished. That distinction saves time.

Cloud images, metadata, and user-data

Keep these three pieces separate. A cloud image is an operating system image prepared for cloud use. Metadata contains information such as the machine name, instance ID, network details, and provider-specific values. User-data is the configuration you send.

Providers expose this information in different ways. OpenStack and many commercial cloud platforms use a metadata service. In my home Proxmox lab, I use the NoCloud datasource. With NoCloud, a seed ISO or suitable disk contains user-data and meta-data files.

Not every virtual machine on Proxmox is automatically ready for cloud-init. You need a cloud image, a template, a cloud-init drive, and values such as the IP address, gateway, and DNS servers. That preparation is done once. Afterwards, you can create machines from the template and provide different metadata or user-data.

I once rushed template validation and cloned a powered-off VM without testing the result. The clone booted, but its hostname and network settings were not what I expected. The serial console still showed details belonging to the old machine. The problem was not in user-data; I had used the template without cleaning it properly or opening a test clone.

These days, every template gets a small test clone. I check its hostname, network, instance identity, and SSH access before allowing it to become a production source.

Your first useful user-data file

User-data is commonly written as YAML and starts with #cloud-config. YAML cares about indentation, so use spaces rather than tabs. I validate the file locally and then try it on a small test VM. One misplaced space can be enough for a module to be skipped.

#cloud-config
hostname: web-01
manage_etc_hosts: true

users:
  - name: deploy
    gecos: Deployment User
    groups: [adm, sudo]
    shell: /bin/bash
    sudo: "ALL=(ALL) NOPASSWD:ALL"
    lock_passwd: true
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... user@laptop

package_update: true
package_upgrade: false
packages:
  - nginx
  - curl
  - vim

write_files:
  - path: /etc/motd
    owner: root:root
    permissions: '0644'
    content: |
      Managed by cloud-init

runcmd:
  - systemctl enable --now nginx
  - [ sh, -c, "echo 'cloud-init complete' >> /var/log/first-boot.log" ]

Here, the deploy user cannot log in with a password, uses an SSH key, and has sudo access. I shortened the real key for display. User-data may be stored in a provider panel, a metadata service, or a seed ISO, so never put your private key in it.

package_update refreshes package lists, while packages installs the packages you specify. Setting package_upgrade: true can upgrade the entire system during first boot. I do not enable that casually on production templates, because a kernel or critical library update may require a reboot.

A safer starting point for users

Many cloud images include a default user. On Ubuntu it is often ubuntu; on Debian images it may be debian. Rather than enabling direct SSH access for root without checking the provider’s documentation, I prefer creating a personal administration user in the first user-data file.

To disable password-based SSH authentication, you can use:

ssh_pwauth: false

users:
  - default
  - name: sysadmin
    groups: [sudo]
    shell: /bin/bash
    lock_passwd: true
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... sysadmin@workstation

The users: - default entry keeps the cloud image’s default user. If you remove it, you may lose the distribution’s initial access account. Changing the SSH port is not security by itself; key-based access, disabled password authentication, current packages, and firewall rules need to be considered together.

Writing files, starting services, and runcmd traps

write_files is useful when you need to place specific content in specific files during first boot. An Nginx site definition, a systemd unit, or a small application configuration can all be installed this way.

write_files:
  - path: /etc/systemd/system/example.service
    permissions: '0644'
    content: |
      [Unit]
      Description=Example service
      After=network-online.target

      [Service]
      Type=simple
      ExecStart=/usr/local/bin/example
      Restart=on-failure
      User=deploy

      [Install]
      WantedBy=multi-user.target

runcmd:
  - [ systemctl, daemon-reload ]
  - [ systemctl, enable, --now, example.service ]

Make sure the binary actually exists before starting the service. write_files creates the file; it does not install the program. runcmd records commands for execution and normally runs them during the final stage.

Shell variables and special characters are another trap. This may not produce what you expect:

runcmd:
  - echo $HOME > /tmp/home.txt

The command is interpreted by a shell during cloud-init’s execution, not necessarily in the environment you had in mind. Make the shell and quoting explicit:

runcmd:
  - [ sh, -c, 'printf "%sn" "$HOME" > /tmp/home.txt' ]

Design commands so that running them again does not damage the system. An echo ... >> command that appends a line on every run creates needless duplicates if you reuse it on the same instance. I have done exactly that in a test image (the log was not dangerous, just embarrassing).

Cloud-init is a good fit for initial setup. When you need ongoing configuration management, moving to Ansible is cleaner. You can use the steps from How to Install Docker on a VPS and Run Your First Container as a starting point for user-data, but test when the Docker service and group changes become effective before putting that setup on a live system.

Cloud-init is not a traditional install script

At first glance, both appear to run commands when a server starts. Cloud-init is tied to the operating system’s boot process and cloud metadata. It can use the instance ID, network information, and SSH keys supplied by the provider; it also runs modules at defined stages and keeps state for many operations.

Method Strength What to watch
Cloud-init Sets up users, packages, and basic system configuration at first boot Requires a cloud image and a correctly configured datasource
Shell script Simple and quick for a prototype Error handling, reruns, and ordering are your responsibility
Ansible Repeatable, readable, and able to manage existing machines Requires SSH access, Python, and a separate control machine
Terraform Defines infrastructure resources such as VPS instances and networks Not enough by itself for detailed operating system configuration

My practical workflow is usually to create the VPS with Terraform or the provider’s panel, prepare initial access and basic security with cloud-init, and then install the application layer with Ansible. For one VPS, using both YAML and Ansible can feel excessive. If you have repeated the same setup manually three times, though, the cost of another manual run is already showing.

When cloud-init appears to fail

Logs are the first place I look. Rebooting the server often does not fix the problem; it may only make the original cause harder to see.

cloud-init status --long
cloud-init query ds
journalctl -u cloud-init -u cloud-config -u cloud-final --no-pager
less /var/log/cloud-init.log
less /var/log/cloud-init-output.log

cloud-init status --long showing status: done tells you that processing finished. It does not tell you that the configuration was correct. cloud-init-output.log is especially useful for finding runcmd output and standard error from commands.

I once saw “done”, assumed everything had succeeded, and reported that the application was ready without checking the service. Later lines in the log showed that the package repository could not resolve its DNS name. The status was accurate; my interpretation was not.

cloud-init query ds may not return identical output on every image. With NoCloud, the instance ID in the metadata file and the connection of the seed disk both matter. Cloning a snapshot or template with the same instance ID can also make cloud-init believe it has already seen the machine.

You can check the YAML syntax locally with:

python3 - <<'PY'
import sys
try:
    import yaml
except ImportError:
    print("PyYAML is not installed", file=sys.stderr)
    sys.exit(1)
with open("user-data", encoding="utf-8") as f:
    yaml.safe_load(f)
print("YAML is valid")
PY

This only checks YAML syntax. It does not verify that the sudo value is correct, that a package exists in the distribution repository, or that a command will run as the user you intended.

Does it run once, or can it run again?

Cloud-init keeps an instance ID and state files for many modules. If you change user-data and reboot the same VPS, do not expect every setting to be applied again. During testing, you can reset it with:

sudo cloud-init clean --logs --seed
sudo reboot

Do not run this randomly on a production machine. It can make cloud-init behave like a new first boot and trigger package, user, or service operations from user-data again. A method that is useful on a test VM is not a production fix without a maintenance plan.

Creating a new instance is often safer than deleting state information from an existing machine. When preparing a template, clean cloud-init and make sure old SSH host keys and machine-specific credentials are not baked into the image.

Security details that matter in production

Do you know where your user-data is stored? On some cloud platforms, authorized processes can access the instance metadata service. User-data may also remain in panel history, Terraform state files, or debug output. Do not write passwords, API keys, or private SSH keys in plain text.

  • Use key-based SSH access instead of passwords.
  • Give the initial user only the sudo access it needs; limit privileged commands where practical.
  • If an API key is required, use a secret manager, the provider’s secret mechanism, or a short-lived token.
  • Test that firewall rules installed by cloud-init do not cut off the current SSH session.
  • Keep the cloud image and cloud-init package current, but do not roll an uncontrolled upgrade across every production machine.
  • Check first-boot logs to make sure they do not contain tokens, passwords, or private keys.

If you want to automate the basic hardening steps, split the recommendations from 10 Essential Steps to Secure and Harden Your Linux Server between user-data and Ansible tasks. I configure the firewall and SSH settings as early as practical with cloud-init, then manage application details later. If first boot fails, access and diagnostic paths remain easier to understand.

A realistic VPS workflow

Suppose you are creating a web server from an Ubuntu 24.04 cloud image. First check the provider’s cloud-init support, the accepted user-data format, and whether network settings are supplied automatically. Then begin with a small configuration: one user, one SSH key, a hostname, and one test package.

#cloud-config
hostname: web-01
fqdn: web-01.example.net
manage_etc_hosts: true
ssh_pwauth: false
package_update: true
packages:
  - nginx

users:
  - default
  - name: deploy
    groups: [sudo]
    shell: /bin/bash
    lock_passwd: true
    sudo: "ALL=(ALL) NOPASSWD:ALL"
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... deploy@laptop

runcmd:
  - [ systemctl, enable, --now, nginx ]
  - [ sh, -c, "printf '%sn' 'first boot ok' > /var/log/first-boot-check.log" ]

After connecting to the machine, do not only check that Nginx is running. Verify that the user can log in with the key, the hostname is correct, the apt operation has finished, and cloud-init has not recorded an error:

hostnamectl
id deploy
sudo systemctl is-active nginx
sudo cloud-init status --long
sudo tail -n 30 /var/log/cloud-init-output.log

You can turn these checks into a shell script or an Ansible verification task. For sensitive services such as MySQL, I prefer setting up the base system first and then using configuration management rather than putting a long runcmd list in one file. The steps in How to Install and Secure MySQL on a VPS are better treated as a later configuration stage. Putting the MySQL root password in user-data is an especially bad idea.

Which jobs belong in cloud-init?

Tasks suited to first boot should be short, predictable, and related to basic access to the machine. Creating a user, installing an SSH key, setting the hostname and timezone, configuring package repositories, installing a few packages, and enabling a simple systemd service are good candidates.

Putting an entire application deployment into one runcmd block creates a fragile system. Use a separate process for long Docker Compose deployments, database schemas, external-service secrets, and migrations that need rollback when they fail. Cloud-init may continue to later commands after a command fails, so handle that explicitly with shell logic.

runcmd:
  - [ sh, -c, 'set -euxo pipefail; /usr/local/bin/bootstrap-app' ]

set -euxo pipefail can make debugging easier, but it is not suitable blindly for every script. Variables containing secrets may be written to logs by -x. In production user-data, balance useful error visibility against confidentiality.

When preparing an image, clean cloud-init, make sure every clone behaves like a new instance, and plan for SSH host keys to be regenerated. Rushed Proxmox operations, such as cloning before the template is properly shut down, can leave two machines showing the same hostname or old network information. I shut down the template, create a test clone, and verify it through both the serial console and SSH.

A sensible place to start

Choose one Ubuntu or Debian cloud image offered by your provider. Trying to manage two distributions with the same user-data creates unnecessary confusion around package names and default users. Write a small file first. Do not move on to Nginx, Docker, or a database until the user and SSH key work.

If you keep the file in version control, never commit real keys or secrets. Monitor the template’s cloud-init logs and service state. A successful first boot means more than a VPS answering ping: secure access with the expected user, the correct hostname, completed package installation, and clean logs all matter.

For me, cloud-init is the first-boot automation layer that turns VPS provisioning into a repeatable process with less manual intervention. I do not put every job inside it. A short, tested user-data file followed by Ansible for the application layer is usually easier to understand than one enormous bootstrap script.

I still perform the final checks. Automation does not replace a checklist; it makes the checklist less dependent on memory.

Frequently asked questions

Does cloud-init work on every VPS?

No. Your provider must support cloud-init, and the selected cloud image and datasource must be configured correctly. A VPS installed from a traditional ISO may not have cloud-init ready automatically.

Is it safe to set a password with cloud-init?

Keeping a password in plain text inside user-data is not safe; it may remain in panel history or a metadata service. Use SSH keys, disable password-based SSH access, and use a separate secret-management mechanism when needed.

Do cloud-init commands run after every reboot?

Usually not. Many modules use the instance ID and state records to run once during first boot. If you need to run something again, test the module behavior and the effect of cloud-init clean on a test VM first.

What is the difference between cloud-init and Ansible?

Cloud-init prepares the basic system and access during the machine’s first boot. Ansible manages existing machines over SSH with more detailed, repeatable, ongoing configuration. In practice, using them one after the other is healthier for many environments.

Avatar of Defne
Author

Defne