Use cloud-init to Bootstrap a Self-Hosting VPS Repeatably
Turn first-boot VPS setup into a small, reviewable config file so rebuilding the base box stops depending on memory and shell history.
How to write a practical #cloud-config file, pass it as user-data, verify what cloud-init actually did, and decide where cloud-init should stop and a fuller config-management tool should begin.
Fresh VPS provisioning where you want a reliable base server shape before you move on to app deployment or Ansible.
cloud-init runs mainly at first boot. If you treat it like a general rerunnable automation engine, you will confuse yourself fast.
Before you begin
- A VPS provider or virtualization platform that accepts cloud-init user-data.
- An SSH public key you already trust.
- A basic understanding that user-data is usually consumed during first boot, not every future reboot.
- A provider console or serial console for recovery if bootstrapping goes wrong.
cloud-init is the standard initialization layer on many cloud images. Its current docs describe cloud-config as one of the supported input formats and provide YAML examples for common tasks such as package installation, file creation, and command execution. The practical win is straightforward: you can give a fresh server a clean baseline automatically instead of repeating the same package installs, user setup, and file edits by hand.
Step 1: Decide what cloud-init should own
cloud-init is strongest at first-day bootstrap, not long-term drift control. Good first responsibilities include:
- Installing a handful of base packages.
- Creating or adjusting an admin user.
- Adding your SSH key.
- Writing one or two small config files.
- Running a short first-boot command sequence.
Do not cram your full application deployment into user-data just because you can. The right use of cloud-init is to get a fresh machine to a safe, consistent starting line.
Step 2: Write a minimal cloud-init config
Create a file such as user-data.yaml on your local machine:
#cloud-config
package_update: true
package_upgrade: true
packages:
- curl
- git
- ufw
- fail2ban
users:
- default
- name: deploy
groups: [sudo]
shell: /bin/bash
sudo: "ALL=(ALL) NOPASSWD:ALL"
ssh_authorized_keys:
- ssh-ed25519 AAAA...replace-with-your-real-key
write_files:
- path: /etc/motd
permissions: "0644"
content: |
Managed by cloud-init.
Verify this host before deploying apps.
runcmd:
- ufw allow OpenSSH
- ufw allow 80/tcp
- ufw allow 443/tcp
- ufw --force enable
- systemctl enable fail2ban
- systemctl start fail2banThis example uses the modules that cloud-init officially documents most often: package actions, users, file writing, and command execution. Keep the file readable. If your first user-data already spans hundreds of lines, you probably crossed the boundary where another tool should take over.
Step 3: Launch the VPS with user-data
How you attach user-data depends on the provider:
- Some dashboards have a dedicated cloud-init or user-data text box.
- Some APIs accept a file reference or raw string.
- Some virtualization tools attach a NoCloud seed or metadata drive.
The important operational rule is that the config should be attached before first boot. If you create the server without it and add it later, you may miss the initialization window you thought you were testing.
Label the server clearly when you launch it so you can distinguish a bootstrap test machine from a long-lived production box.
Step 4: Verify the first boot properly
After the VPS comes up, do not assume the config worked just because the machine exists. cloud-init provides its own logs and status commands for validation.
cloud-init status --long
sudo cloud-init status --wait
sudo tail -n 100 /var/log/cloud-init.log
sudo tail -n 100 /var/log/cloud-init-output.logThen verify the actual outcomes you intended:
id deploy
sudo ufw status verbose
systemctl status fail2ban --no-pager
grep -n 'Managed by cloud-init' /etc/motdThis is the difference between “cloud-init ran” and “my desired state exists.” Both matter.
Step 5: Know when to graduate to Ansible
cloud-init is excellent for first boot. It is not the cleanest tool for repeated day-two configuration changes across many hosts. Once you need rerunnable playbooks, drift correction, or app-specific orchestration, move that responsibility into something like Ansible.
A healthy split often looks like this:
- cloud-init: user, key, updates, baseline packages, one-time bootstrap files.
- Ansible or similar: repeatable long-term server configuration and application roles.
That boundary keeps each tool doing the job it is best at.
Rollback and recovery notes
If bootstrapping fails badly, treat the machine as disposable unless it already contains irreplaceable data. Rebuilding a broken fresh VPS from a corrected cloud-init file is often faster and safer than hand-repairing every mistake.
If you do need to inspect and salvage the machine:
- Use the provider console if SSH never became reachable.
- Read cloud-init logs before changing files so you know what actually happened.
- If the box is still empty, prefer destroy-and-recreate over drifting away from your intended bootstrap.
The point of this workflow is repeatability. Quiet manual fixes that never get folded back into user-data defeat that purpose.
Troubleshooting common cloud-init problems
The VPS booted, but my user or key was not created.
Check YAML indentation first. Small spacing errors are one of the most common causes of cloud-config failure.
The commands in runcmd did not do what I expected.
Read /var/log/cloud-init-output.log and verify whether the commands ran, failed, or assumed a package or network state that was not ready yet.
I changed the user-data file, but the existing server did not pick up the change.
That is usually expected. cloud-init is primarily a first-boot tool. Rebuild or follow the documented cloud-init cleaning and rerun workflows only when you understand the consequences.
The machine is reachable, but the final state still looks wrong.
Validate the actual artifacts, not just the service status. For example, confirm the file contents, firewall state, installed packages, and SSH user behavior individually.
What to do next
Once first-boot bootstrap is repeatable, the next useful layer is longer-lived configuration ownership. Continue with How to Provision a Self-Hosting VPS with Ansible.
