Back to tutorials
Tutorial

Cloud-Init Setup Guide Tutorial (2026): Automate Secure VPS Provisioning on Ubuntu, Debian, AlmaLinux & Rocky

Cloud-init setup guide tutorial for repeatable VPS builds: users, SSH keys, updates, firewall basics, and first-boot validation.

By Anurag Singh
Updated on Sep 03, 2026
Category: Tutorial
Share article
Cloud-Init Setup Guide Tutorial (2026): Automate Secure VPS Provisioning on Ubuntu, Debian, AlmaLinux & Rocky

It’s easy to burn an hour on the same “new server” routine. You add a user, harden SSH, run updates, and set a hostname. Then you wonder what you missed. A cloud-init setup guide tutorial fixes that by moving first-boot setup into a file you can version, reuse, and review.

This walkthrough gives you a practical baseline for hosting VPS builds on Ubuntu, Debian, AlmaLinux, and Rocky Linux in 2026. You’ll create a cloud-init config that adds an admin user, installs a small toolset, applies updates, enables basic protection, and writes clear logs for troubleshooting.

What you’ll build (and why it matters for hosting)

Cloud-init runs once on first boot and applies the configuration you provide. That config is usually called “user-data.” In hosting, that matters because:

  • You can provision 10 servers with the same hardening baseline, not 10 hand-built snowflakes.
  • You can rotate SSH keys and disable passwords at creation time, before the box ever accepts logins.
  • You cut setup time for new client VPS and reseller nodes.
  • You get consistent logs (cloud-init records what it did), which speeds up troubleshooting.

If you want a clean VPS with root access for automation and performance tuning, start with a HostMyCode VPS. If you’d rather hand off patching and baseline security, managed VPS hosting is a better match.

Prerequisites and safe defaults

  • A newly created VPS (cloud-init is most effective at first boot).
  • Your public SSH key (ed25519 recommended) on your workstation.
  • A firewall plan: UFW (Ubuntu/Debian) or firewalld (AlmaLinux/Rocky).

Keep two constraints in mind:

  • Don’t lock yourself out: allow SSH (port 22 or your custom port) before turning on a default-deny firewall.
  • Plan for distro differences: package names and firewall tools vary; keep your baseline portable.

Step 1: Generate an SSH key (if you don’t have one)

On your local machine, create an ed25519 keypair. If you already manage keys, skip this step.

ssh-keygen -t ed25519 -a 64 -f ~/.ssh/hostmycode-admin

Copy the public key:

cat ~/.ssh/hostmycode-admin.pub

You’ll paste that key into cloud-init. With key-based access in place, you can disable password logins immediately.

If you want a tighter SSH hardening workflow (including how to avoid lockouts), see SSH key setup guide.

Step 2: Write a cloud-init baseline (user, SSH, packages, updates)

Create a file named user-data.yaml on your workstation.

This baseline stays conservative on purpose. It improves security without assuming your full application stack.

#cloud-config

# Identify the host clearly in logs and monitoring
hostname: vps-web-01
manage_etc_hosts: true

# Keep the server lean but usable for hosting administration
package_update: true
package_upgrade: true
packages:
  - curl
  - wget
  - vim
  - git
  - ca-certificates
  - unattended-upgrades
  - fail2ban

# Create a non-root admin user
users:
  - default
  - name: admin
    gecos: HostMyCode Admin
    groups: [sudo, adm]
    shell: /bin/bash
    sudo: ["ALL=(ALL) NOPASSWD:ALL"]
    lock_passwd: true
    ssh_authorized_keys:
      - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...REPLACE_ME... yourname@laptop"

disable_root: true
ssh_pwauth: false

# Optional: set timezone for accurate logs
timezone: UTC

# Write a small marker file so you can confirm cloud-init ran
write_files:
  - path: /etc/hostmycode-provisioned
    permissions: '0644'
    content: |
      provisioned_by=cloud-init
      provisioned_at=${timestamp}

# Commands run at the end of boot. Keep these short and idempotent.
runcmd:
  - [ bash, -lc, "sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config" ]
  - [ bash, -lc, "sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config" ]
  - [ bash, -lc, "systemctl restart ssh || systemctl restart sshd" ]
  - [ bash, -lc, "systemctl enable --now fail2ban" ]
  - [ bash, -lc, "echo 'cloud-init baseline complete'" ]

final_message: "Cloud-init finished in $UPTIME seconds"

Why these choices:

  • lock_passwd: true and ssh_pwauth: false block password logins.
  • disable_root: true forces you onto a named admin account.
  • Fail2Ban is enabled, but you should tune jails later based on your services.
  • Unattended upgrades help, but production hosting nodes still need a reboot plan for kernel updates.

Step 3: Add firewall bootstrap (UFW on Ubuntu/Debian; firewalld on AlmaLinux/Rocky)

A typical hosting VPS needs SSH plus HTTP/HTTPS at minimum. Mail ports depend on whether you run email on the same box.

Cloud-init can apply a firewall baseline. Keep the first pass simple, or you risk blocking legitimate traffic.

Option A: UFW (Ubuntu/Debian)

Add these lines under runcmd: if your distro uses UFW:

  - [ bash, -lc, "apt-get -y install ufw" ]
  - [ bash, -lc, "ufw default deny incoming" ]
  - [ bash, -lc, "ufw default allow outgoing" ]
  - [ bash, -lc, "ufw allow 22/tcp" ]
  - [ bash, -lc, "ufw allow 80/tcp" ]
  - [ bash, -lc, "ufw allow 443/tcp" ]
  - [ bash, -lc, "ufw --force enable" ]

If you later hit blocked SSL renewals or mail ports, keep this reference handy: UFW firewall troubleshooting tutorial.

Option B: firewalld (AlmaLinux/Rocky)

On RHEL-family systems, use firewalld. Add this under runcmd::

  - [ bash, -lc, "dnf -y install firewalld" ]
  - [ bash, -lc, "systemctl enable --now firewalld" ]
  - [ bash, -lc, "firewall-cmd --permanent --add-service=ssh" ]
  - [ bash, -lc, "firewall-cmd --permanent --add-service=http" ]
  - [ bash, -lc, "firewall-cmd --permanent --add-service=https" ]
  - [ bash, -lc, "firewall-cmd --reload" ]

Step 4: Feed cloud-init into your VPS provisioning

How you supply user-data depends on how you deploy the server:

  • Hosting control panel / provider UI: many VPS panels include a “cloud-init” or “user-data” field during creation. Paste the YAML directly.
  • Private virtualization (KVM/Proxmox/OpenStack): you typically attach a “NoCloud” ISO or set metadata through your platform.
  • Existing server: cloud-init targets first boot, but you can still test your logic in a VM before using it for new production nodes.

Two rules travel well across providers. Your YAML must start with #cloud-config. Indentation must be exact.

A single misplaced space can change what runs.

Step 5: Verify cloud-init ran (fast checks that catch real mistakes)

After the VPS boots, SSH in as your admin user. Avoid root logins entirely.

ssh -i ~/.ssh/hostmycode-admin admin@YOUR_SERVER_IP

Then run the checks below.

Check cloud-init status and logs

sudo cloud-init status --long

If anything looks wrong, go straight to the logs:

sudo tail -n 200 /var/log/cloud-init.log
sudo tail -n 200 /var/log/cloud-init-output.log

Confirm SSH is locked down

sudo sshd -T | egrep 'passwordauthentication|permitrootlogin'

You want passwordauthentication no and permitrootlogin no.

Confirm your marker file exists

cat /etc/hostmycode-provisioned

Quick firewall sanity check

UFW:

sudo ufw status verbose

firewalld:

sudo firewall-cmd --list-all

Step 6: Add a “hosting-ready” first-boot checklist (swap, time sync, basic monitoring hooks)

Cloud-init can handle more than user creation.

The sweet spot is low-risk tasks that stay consistent across many servers.

6A) Ensure time sync is active

Accurate time prevents strange TLS failures and messy log correlation. It also avoids a few email-delivery edge cases.

timedatectl status

On most modern distros, systemd-timesyncd is enough. If you standardize on chrony, install it via the packages list.

6B) Create a small swap file for bursty hosting workloads (optional)

On small VPS plans, a modest swap file can prevent hard OOM kills during traffic spikes or PHP compile events.

If you run latency-sensitive workloads, keep swap small and monitor it.

Add under runcmd: (Ubuntu/Debian):

  - [ bash, -lc, "fallocate -l 1G /swapfile" ]
  - [ bash, -lc, "chmod 600 /swapfile" ]
  - [ bash, -lc, "mkswap /swapfile" ]
  - [ bash, -lc, "swapon /swapfile" ]
  - [ bash, -lc, "grep -q '^/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab" ]

Verify:

swapon --show
free -h

6C) Add a basic health endpoint for uptime checks (optional)

External monitoring catches the failures users notice first. That includes DNS problems, expired SSL, web server outages, and disks filling up.

If you don’t have a standard approach yet, follow Uptime Monitoring Tutorial (2026) and apply it across your fleet.

Step 7: Common cloud-init mistakes (and quick fixes)

Most “cloud-init didn’t work” reports come down to a few repeat offenders.

YAML indentation errors

  • Symptom: user not created, SSH key not applied.
  • Fix: check /var/log/cloud-init.log for parse errors; validate YAML indentation. Lists must align.

Wrong SSH service name

  • Symptom: your systemctl restart ssh fails on RHEL-family hosts.
  • Fix: use systemctl restart ssh || systemctl restart sshd as shown.

Firewall enabled before SSH allow rule

  • Symptom: you get locked out immediately.
  • Fix: in cloud-init, add allow rules first, then enable the firewall. On critical servers, keep provider console access available.

Package names differ by distro

  • Symptom: unattended-upgrades missing on non-Debian systems.
  • Fix: maintain a Debian/Ubuntu user-data and a RHEL-family user-data. Don’t force a single file if it becomes brittle.

Step 8: Extend the baseline for common hosting roles

After your baseline proves itself, keep role configs small. Small files create fewer surprises than one giant, do-everything config.

Here are two variants that map cleanly to real hosting work.

Variant A: WordPress + web server node

  • Install Nginx/Apache and PHP packages (or your control panel’s stack).
  • Open ports 80/443 and keep SSH restricted.
  • Plan your SSL automation. If renewals fail, use SSL renewal troubleshooting as your runbook.

If you’re building WordPress on a VPS without a control panel, this workflow pairs well with cloud-init: WordPress VPS setup guide tutorial (2026).

Variant B: Email-sending node (transactional mail, app notifications)

  • Make rDNS/PTR planning part of your provisioning checklist.
  • Set SPF/DKIM early so you don’t “warm up” a misconfigured sender.
  • Monitor the queue and logs from day one.

Use these follow-ups when mail bounces or lands in spam: email deliverability troubleshooting and mail queue troubleshooting.

Step 9: Treat cloud-init like code (version, review, and rollback)

The payoff comes when you stop pasting random snippets into provider forms.

Treat your cloud-init baseline like you would any other config you rely on.

  • Keep one directory per distro family (debian/ and rhel/), plus role overlays (web/, mail/, cpanel/).
  • Store public keys and usernames in variables if your platform supports templating; otherwise, keep a documented edit step.
  • Require a restore path. Standard builds make restores predictable.

For backup automation and restore testing on VPS, follow VPS backup automation. For disaster recovery planning (snapshots + offsite + DNS), see VPS disaster recovery tutorial.

Summary: a repeatable provisioning flow you can trust

A good cloud-init file does three jobs. It establishes admin access safely, applies sensible updates, and leaves logs you can audit.

Use the baseline here, then split into role-specific variants as your hosting fleet grows.

If you’re standardizing builds across client projects, pick a plan with predictable CPU and disk performance. Use a HostMyCode VPS for full control, or choose managed VPS hosting if you want help with ongoing maintenance.

If you provision servers regularly, cloud-init turns “new VPS setup” into a repeatable runbook you can execute in minutes. HostMyCode offers options for both hands-on admins and teams that want support included via HostMyCode VPS or managed VPS hosting.

FAQ

Does cloud-init work on all VPS providers?

Most modern VPS images ship with cloud-init, but provider support varies. The quick test is cloud-init status after first boot. If it’s missing, install cloud-init and use it on your next rebuild.

Can I use cloud-init on an existing production server?

Cloud-init is designed for first boot. You can re-run modules manually, but it’s safer to apply changes with normal config management and reserve cloud-init for new instances and rebuilds.

Should I enable automatic updates in 2026?

For internet-facing hosting nodes, unattended security updates are usually a net win. Still, you need a reboot plan for kernel updates and a monitoring alert when a reboot is pending.

What’s the safest way to avoid SSH lockouts?

Put your SSH key into cloud-init, verify console access exists (provider console/VNC), and only then disable passwords and root login. Always allow SSH in the firewall before enabling default-deny rules.

Where do I look first when cloud-init doesn’t apply my config?

Start with /var/log/cloud-init.log and /var/log/cloud-init-output.log. Most failures are YAML formatting, a missing package name, or a command that doesn’t exist on that distro.