GICT knowledge base
What Is Cloud-Init and How Does It Work? The Complete Guide
Published
You just spun up a new VPS with a fresh Ubuntu install. The control panel shows it is online. You have the IP address and a root password. Now you need to: create a non-root user with sudo access, paste in your SSH public key, install ufw and open port 22, install the Netdata agent, and set the hostname.
You can do all of that by hand. Or you can hand the machine a YAML file and have it done before your SSH client finishes resolving the hostname.
cloud-init is the tool that makes the second option work. It runs on every major public cloud, on cloud VPS images from providers like InterServer, and on bare-metal servers you provision yourself. It reads declarative configuration and executes it during boot. This is not magic. It is a structured pipeline with known stages, known inputs, and known outputs. Once you see how it fits together, you can use it on any Linux system you control.
The Five Boot Stages
cloud-init does not run all at once. It runs in five sequential stages, and each stage has a specific job.
Stage 1 is Detect. A utility called ds-identify figures out which cloud platform the system is running on. If ds-identify finds a valid platform, cloud-init stays enabled. If it finds nothing, cloud-init disables itself. You can override this by specifying a datasource directly in the configuration.
Stage 2 is Local. This stage runs as early as possible, before the network is fully up. It looks for local datasources and applies early network configuration. It must block boot until it finishes because the results of this stage affect how the system reaches the network.
Stage 3 is Network. This stage runs after the network is online. It processes user data and runs the modules listed under cloud_init_modules in /etc/cloud/cloud.cfg. The disk_setup and mounts modules run here because they may depend on network-provided configuration. By the end of this stage, SSH is accessible and serial console login works.
Stage 4 is Config. This stage runs after Network and does not block boot. It runs the modules listed under cloud_config_modules. The runcmd module runs here, which means commands you define in cloud-config execute at this point.
Stage 5 is Final. This is the last boot stage. It runs as late as possible, equivalent to what rc.local used to do. It runs modules listed under cloud_final_modules, including package installations and any custom scripts you pass as user data.
What Goes In: User Data, Metadata, Vendor Data
cloud-init reads three types of input at boot time. User data is what you provide. It defines the desired state of the system: which users to create, which packages to install, which commands to run. Metadata comes from the cloud platform and includes things like the instance ID and local hostname. Vendor data is optional and comes from the hosting provider. It typically contains repository mirrors and other defaults that you can override with your own user data.
cloud-init merges these three sources, with user data taking the highest priority. Your settings override vendor defaults. This means a cloud provider can give you sensible OS-level defaults while still letting you override everything.
The critical file that tells cloud-init how to find these inputs is called a datasource. Different cloud platforms provide different datasources. AWS uses the EC2 metadata service at 169.254.169.254. OpenStack uses a config drive or metadata service. NoCloud, which is the focus of the next section, lets you provide everything from local files or an HTTP server.
The NoCloud Datasource: Using cloud-init Without a Cloud Provider
NoCloud is a datasource that does not require any cloud platform. You can use it on a bare VPS, a KVM virtual machine, or a physical server. It delivers configuration through three mechanisms: kernel command line, files in /etc/cloud/cloud.cfg.d/, or a CIDATA volume.
The kernel command line approach is useful for VMs you launch programmatically. Pass this to your hypervisor:
ds=nocloud;s=https://10.0.2.42/cloud-init/
The ds=nocloud part tells cloud-init to use the NoCloud datasource. The s= part specifies a seedfrom URL, pointing to a directory on an HTTP server that contains the configuration files. The trailing slash is required. The server must serve two files: user-data and meta-data.
A second approach is to put the configuration directly in /etc/cloud/cloud.cfg.d/. Create a file ending in .cfg with these contents:
datasource:
NoCloud:
seedfrom: https://10.0.2.42/cloud-init/
cloud-init reads every file in /etc/cloud/cloud.cfg.d/ that ends in .cfg on boot. This is the simplest approach for a VPS where you have filesystem access before first boot.
A third approach uses a CIDATA volume. Label a VFAT or ISO9660 filesystem with the label CIDATA, put user-data and meta-data files in its root directory, and attach it to the VM. cloud-init detects this volume automatically. This is how cloud images are typically distributed on media that does not have a built-in datasource.
NoCloud also supports a powerful feature called DMI variable expansion. If your HTTP server is behind a firewall and you do not want to open it to all IPs, use the chassis serial number from DMI to scope the URL. On the kernel command line:
ds=nocloud;s=http://10.0.2.42/__dmi.chassis-serial-number__/
cloud-init reads the DMI variable from /sys/class/dmi/id/chassis_serial_number and substitutes it into the URL. Your HTTP server can serve different configuration to different machines based on their serial numbers.
Writing Your First cloud-config File
cloud-config is YAML with a required header. Every cloud-config file must start with this line:
#cloud-config
Without that header, cloud-init treats the file as a shell script instead of a cloud-config document. The difference matters: shell scripts run arbitrary commands, cloud-config uses declarative keys that cloud-init maps to specific modules.
Here is a complete cloud-config that sets up a production-ready VPS:
#cloud-config
hostname: web-01
manage_etc_hosts: true
groups:
- adm
- sudo
users:
- default
- name: deploy
gecos: Deploy user
primary_group: deploy
groups: sudo, adm
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_authorized_keys:
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDT8...
lock_passwd: true
packages:
- ufw
- curl
- wget
runcmd:
- ufw --force enable
- ufw allow 22/tcp
- ufw allow 80/tcp
- ufw allow 443/tcp
- systemctl enable netdata
- systemctl start netdata
write_files:
- encoding: b64
content: CiMgVGhpcyBmaWxlIGNvbnRhaW5zIGEgc2VjcmV0IGZyb20gY2xvdWQtaW5pdAo=
owner: root:root
path: /etc/secrets/app-key
permissions: '0600'
The users section creates a deploy user with sudo rights and your SSH key installed. The default user from the distro image is preserved because we listed - default. Without that line, cloud-init replaces the default user instead of adding to it.
The runcmd module takes a list of commands and runs them in the Config stage. Each entry in the list can be a string or a list. When it is a string, cloud-init passes it to sh. When it is a list, cloud-init treats the first element as the command and the rest as arguments, bypassing a shell layer. The list form is safer and avoids quoting issues.
The write_files module writes files to disk during the Config stage. The content field accepts plain text, base64-encoded data, gzipped data, or gzipped and base64-encoded data. This is how you inject SSL certificates, configuration files with secrets, and other sensitive data without putting it in plain YAML.
Module Frequency: When Each Module Runs
Every cloud-init module has a frequency that determines when it runs. There are three possible values.
PER_ALWAYS means the module runs on every boot. The bootcmd module uses this frequency. Use it for commands that must run every time the system starts, like fixing /etc/hosts entries that tend to get overwritten.
PER_ONCE means the module runs only a single time, regardless of how many boots follow. The once-per-instance frequency from the module docs means the module runs on first boot but also runs again if the instance ID changes. A clone of the original instance triggers it again.
Most modules use once-per-instance frequency. SSH key generation, user creation, and package installation all fall into this category. They run once and cloud-init records their state so they do not re-run unnecessarily.
You can override a module frequency in cloud-config if you need to. Add frequency: [PER_ALWAYS|PER_ONCE|PER_INSTANCE] to the module configuration in your cloud-config file.
Debugging cloud-init
When cloud-init does not do what you expect, the first command to run is this:
cloud-init status --wait
This blocks until cloud-init finishes running all stages and reports the result. If it completes without error, all modules ran successfully. If something failed, the output tells you which stage and module had the problem.
Log files are in /var/log/cloud-init.log. This file records every decision cloud-init made, every module it ran, and every error it encountered. On a system that has been running for a while, the log can be long. Filter it by module name to narrow the output:
grep -i "runcmd\|cc_runcmd" /var/log/cloud-init.log
If cloud-init never runs at all, check whether the datasource is detected. The ds-identify command shows what datasource cloud-init found and why:
sudo ds-identify
This prints the detected datasource, any match criteria that succeeded or failed, and the final decision. If it reports “not found” but you expect a datasource to be present, the issue is in how the VM or cloud environment presents its metadata.
cloud-init Is Already on Your VPS
InterServer VPS images ship with cloud-init installed and enabled. The configuration you write as cloud-config applies automatically on every boot. You do not need a cloud provider to use it. Point NoCloud at a web server, or drop a configuration file into /etc/cloud/cloud.cfg.d/, and cloud-init applies it on the next reboot.
The payoff is reproducibility. When you can describe your server setup in YAML and apply it automatically, you stop treating your running servers as snowflakes. If a node dies, you rebuild from the same configuration and get the same result. This is the foundation of everything from immutable infrastructure to full IaC pipelines.
Ready to automate your VPS setup? InterServer VPS plans start at $6/month with full root access and cloud-init already running on every image. Deploy your first configured instance and hand it a YAML file instead of typing commands by hand.
Related guides
How to Set Up zram on a Small VPS
Configure zram on Ubuntu with kernel and swap checks. Understand compressed memory limits, monitor real use and avoid unsafe swap-file changes.
LinuxHow to Find Services That Need a Restart After Updates with needrestart
Find services using outdated libraries with needrestart on Ubuntu. Read restart reports, review automatic restart policy and plan maintenance safely.
LinuxHow to Limit a Linux Service’s Memory with systemd
Limit a Linux service with systemd MemoryHigh, MemoryMax and swap controls. Check cgroup support, diagnose pressure and remove only your own policy.
LinuxHow to Protect SSH with CrowdSec on Ubuntu
Protect SSH with CrowdSec on Ubuntu. Connect log detection to firewall enforcement and test temporary blocking without losing administrator access.
