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.

cloud-initVPS bootstrapRepeatable first boot
Illustrated guide cover for Use cloud-init to Bootstrap a Self-Hosting VPS Repeatably
cloud-init • VPS bootstrap • Repeatable first boot
What you learn

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.

Best for

Fresh VPS provisioning where you want a reliable base server shape before you move on to app deployment or Ansible.

Risk to watch

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.

Expected outcome: One small config file should leave a new VPS reachable by key, updated, and prepared for later app-specific automation.

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 fail2ban

This 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.

Warning: Replace the SSH key with your real public key before launch. A placeholder key turns this from automation into self-lockout.

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.log

Then verify the actual outcomes you intended:

id deploy
sudo ufw status verbose
systemctl status fail2ban --no-pager
grep -n 'Managed by cloud-init' /etc/motd

This 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.

Expected outcome: A fresh VPS can now start from the same baseline every time, and you know exactly where cloud-init should stop so later automation stays cleaner.

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.