cloud-init for Server Provisioning
cloud-init turns a generic disk image into your server before you ever log in. This guide covers the cloud-config format, the six modules most bootstraps need, a hardened example, where each provider hides the field, and how to debug a silent run.

The Minutes Before Anyone Logs In
Between the moment a cloud instance is created and the moment you first connect, something has to turn a generic disk image into a machine that belongs to you. It has to set the hostname, install your public key, expand the root filesystem to the volume size you paid for and bring up the network.
On nearly every Linux cloud image, the thing doing that work is cloud-init. It runs as a set of systemd units early in boot, reads instructions supplied by the platform, and applies them in a defined order. Most people never notice it, which is a sign it works.
The interesting part is that you can supply your own instructions. Anything you would otherwise type in the first ten minutes of a server's life can be declared in advance, which closes the window where a machine sits on the public internet in its default state.
The Input Format, and Its Two Fussy Rules
Instructions arrive through a field the provider calls user data, custom data or a startup script depending on the platform. The most useful format is cloud-config, a YAML document with one mandatory feature: the very first line must be exactly #cloud-config.
That line looks like a comment and is not. Omit it, or let an editor insert a byte order mark ahead of it, and cloud-init classifies the payload as something else and quietly skips your configuration. This accounts for a large share of the "it did nothing" reports.
The second rule is that YAML indentation is significant and unforgiving. Validate the document before pasting it, ideally with cloud-init schema --config-file user-data.yaml on any machine that has the package installed.
A shell script starting with a shebang line is also accepted and is fine for something trivial. Beyond a handful of commands, cloud-config is easier to read and its modules handle ordering and idempotence for you.
The Handful of Modules That Cover Most Needs
cloud-config is organised into modules, each keyed by a top level name. You rarely need more than six.
| Key | What it does |
|---|---|
users | Creates accounts, sets sudo rules, installs authorized keys |
package_update and packages | Refreshes the index, then installs a named list |
write_files | Drops configuration files with a chosen owner and mode |
runcmd | Runs commands late in boot, after the modules above |
ssh_pwauth | Turns password authentication on or off for the SSH daemon |
power_state | Reboots or shuts down once processing finishes |
Ordering matters and is fixed by cloud-init, not by the order keys appear in your file. Files are written before runcmd executes, which is why the usual pattern is to place a unit file or an application config with write_files and then enable it with a single command afterwards.
A Bootstrap That Produces a Usable Server
A practical first boot configuration creates an unprivileged account with your key, removes password login entirely, patches the image and installs what the workload needs.
users: - name: deploy sudo: "ALL=(ALL) NOPASSWD:ALL" shell: /bin/bash ssh_authorized_keys: - ssh-ed25519 AAAAC3Nz...
ssh_pwauth: false and disable_root: true close the two doors that automated scanners knock on first. Then package_update: true with package_upgrade: true brings the image forward from its build date, because published images are always missing fixes released since they were made.
Finish with the specifics. A short runcmd list can enable a firewall, start a service and write a marker file you can check for later.
runcmd: - ufw allow OpenSSH - ufw --force enable - systemctl enable --now nginx
The result is a machine that reaches a hardened state before its first human connection. The reasoning behind each of those choices is unpacked in securing a new cloud server, and key handling specifically in SSH keys for cloud servers.
Where the Field Hides on Each Platform
The mechanism is shared; the label is not. On AWS the field is called user data and can be supplied at launch or edited while the instance is stopped, as described in the EC2 instance metadata documentation. Azure exposes both custom data and user data on Linux virtual machines. Google Compute Engine accepts a user-data metadata key on images that ship cloud-init.
Developer clouds keep it simpler. DigitalOcean, Vultr, Linode and Hetzner all present a single user data box in the creation form and an equivalent field in their APIs and command line tools, which means the same document works across all of them with no edits.
That portability is the quiet advantage. A bootstrap written once tends to survive a change of provider, unlike a provisioning script tied to one vendor's tooling. If you are testing that claim, the Vultr account listings describe what such an account provides, and the Hetzner versus Vultr comparison looks at how the two present the same field.
When Nothing Appears to Have Happened
Three commands answer almost every question. cloud-init status --long reports whether processing finished and whether it recorded errors. cloud-init query userdata prints what the instance actually received, which immediately settles whether the payload arrived intact.
For detail, read /var/log/cloud-init-output.log. It contains the standard output of every module and command, including the package manager, and it is where a failing runcmd entry confesses.
Remember that cloud-config is applied once, on first boot. Rebooting does not reapply it. To test a change, destroy the instance and create a new one, which is cheap and also proves the configuration works from a clean state rather than from a machine you have already poked at.
If a command needs the network, be aware it may run before DNS is fully settled on some images. Adding a short retry loop around anything that fetches from the internet removes a frustrating class of intermittent failure.
Knowing When to Hand Over
cloud-init is a bootstrap tool, not a configuration management system. It has no concept of ongoing state, no way to correct drift, and no reporting across a fleet. Once a document grows past roughly a screen of YAML, you are writing a fragile installer.
The healthier split is to let cloud-init do the minimum, install your key, harden SSH, install an agent, and then let a dedicated tool own everything after that. Ansible for cloud server configuration covers that handover, and infrastructure as code with Terraform shows how the user data document itself becomes a versioned file rather than a paste into a web form.
Points to Remember
- Begin the document with
#cloud-configon line one, with nothing before it. - Validate the YAML before launch; indentation errors fail silently.
- Create your user and install keys here, not after first login.
- Disable password authentication and root login in the same document.
- Keep secrets out of user data, since the instance and the API can both read it.
- Diagnose with
cloud-init status --longand the output log, not by guessing. - Test changes by creating a fresh instance, because it only runs on first boot.
- Stop at the bootstrap and hand ongoing configuration to a purpose built tool.
Questions about a cloud account?
Ask about products, variants, availability or purchasing on either channel.



