How to Automate Server Setup with cloud-init: Never Manually Configure a VPS Again
cloud-init automates server provisioning on first boot. Here is how to use cloud-config YAML to install Docker, create users, and secure your VPS before you even SSH in.
Every time you spin up a new VPS, you repeat the same steps: create a user, add your SSH key, install Docker, configure the firewall, set the hostname. This takes 20 minutes and is error-prone when you are doing it at 11 PM during an incident. cloud-init automates all of this on first boot. Your server arrives already configured, secured, and ready to deploy applications. Here is how to use it.
What cloud-init Is
cloud-init is the industry standard for cross-platform cloud instance initialisation. It ships by default on Ubuntu and most cloud images. When a cloud instance boots for the first time, cloud-init reads a configuration file (called user-data) and executes the instructions: install packages, create users, write files, run commands, configure networking. By the time you SSH in, the server is already set up exactly how you want it.
cloud-init works on DigitalOcean, Hetzner, AWS, Google Cloud, Azure, and virtually every other cloud provider. It also works locally with QEMU, LXD, Multipass, and Libvirt for testing.
The cloud-config YAML Format
cloud-init configuration is written in YAML with the header #cloud-config on the first line. Here is a production-ready configuration that prepares a server for Docker self-hosting:
#cloud-config
hostname: vps1-server
fqdn: vps1-server.yourdomain.com
timezone: Asia/Brunei
users:
- name: deploy
groups: sudo, docker
shell: /bin/bash
ssh_authorized_keys:
- ssh-ed25519 AAAAC3... your_public_key_here
sudo: ALL=(ALL) ALL
passwd: "$YOURPASSWORDHASH" # generate with: mkpasswd --method=sha-512
packages:
- docker.io
- docker-compose-v2
- ufw
- unattended-upgrades
- curl
- git
- htop
- vim
package_update: true
package_upgrade: true
disable_root: true
ssh_pwauth: false
write_files:
- path: /etc/docker/daemon.json
content: |
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
owner: root:root
permissions: '0644'
runcmd:
- ufw allow 22/tcp
- ufw allow 80/tcp
- ufw allow 443/tcp
- ufw --force enable
- systemctl enable docker
- systemctl start docker
final_message: "Server setup complete. Docker installed, firewall active. Root login disabled, SSH key-only auth enabled." key-only auth enabled. Root login and password authentication disabled."Step-by-Step Breakdown
hostname and timezone
Sets the server's identity. Use a fully qualified domain name if you have one. The timezone ensures logs and cron jobs run at the correct local time. For Brunei, that is Asia/Brunei.
users
Creates a non-root user named deploy, adds it to the sudo and docker groups, and installs your SSH public key. The sudo line grants sudo access. After first boot, you SSH in as deploy@server-ip using your SSH key. No root login. No password login. No shared credentials.
packages
Installs Docker, Docker Compose v2, UFW firewall, unattended-upgrades for automatic security patches, and essential tools. The package_update and package_upgrade directives run apt update and apt upgrade before installing, ensuring you get the latest versions.
write_files
Creates Docker's daemon.json to configure log rotation. Without this, Docker container logs grow indefinitely and fill your disk. The configuration above limits each container to 10MB per log file with a maximum of 3 files, totalling 30MB per container. For 20 containers, that is 600MB maximum for all logs combined.
runcmd
Runs shell commands during the final boot stage. These commands enable the firewall (allowing only SSH, HTTP, and HTTPS), start Docker, and enable the firewall. The disable_root and ssh_pwauth directives (configured above) disable root login and SSH password authentication. After cloud-init finishes, root login is disabled, and the server accepts only SSH key authentication.
How to Use cloud-init
DigitalOcean
When creating a Droplet, expand the Advanced Options section. Check the User data box and paste your cloud-config YAML. DigitalOcean runs cloud-init automatically during the Droplet's first boot.
Hetzner Cloud
When creating a server, scroll to Cloud config. Paste your YAML. The cloud-init package is pre-installed on Ubuntu images. Hetzner also supports cloud-init via their API and CLI.
Testing Locally with Multipass
multipass launch --name test --cloud-init cloud-config.yamlMultipass is a lightweight VM manager for Linux, macOS, and Windows. It is the fastest way to test cloud-init configurations without spinning up a paid cloud instance.
Debugging cloud-init
If something does not work, check the logs:
# Check cloud-init status
cloud-init status
# View the full log
sudo cat /var/log/cloud-init-output.log
# View cloud-init's own log
sudo cat /var/log/cloud-init.log
# Re-run cloud-init (for testing after fixing config)
sudo cloud-init clean --logs
sudo cloud-init init --local
sudo cloud-init init
sudo cloud-init modules --mode=config
sudo cloud-init modules --mode=finalThe status command tells you whether cloud-init completed successfully. The output log shows you exactly what happened during each stage. If a package failed to install or a command returned an error, it appears here.
Note on root access: The cloud-config above disables root login (disable_root: true) and password authentication (ssh_pwauth: false). This means you can only SSH in as the deploy user with your key. If you need to use the root account for any reason, SSH in as deploy first, then switch to root:
sudo su -
passwd # set a root password if neededThe deploy user has sudo access, so you can run any administrator command with sudo without ever logging in as root directly.
Why This Matters for Self-Hosting
Self-hosting means you provision servers more often than most businesses. Every new application might need a new VPS. Every disaster recovery drill requires rebuilding from scratch. Every client deployment starts with a fresh server. cloud-init turns server provisioning from a 20-minute manual checklist into a one-time configuration that runs automatically every time you deploy.
Combine cloud-init with Docker Compose stacks in /opt/stacks, and you have a fully automated deployment pipeline: cloud-init prepares the server (Docker, firewall, users, SSH), and Dockge or docker compose pulls and starts your applications. A new server goes from nothing to fully operational in under five minutes, with zero manual steps after pasting the cloud-config.
VPS1 uses cloud-init to provision every managed server. We maintain cloud-config templates for each client's stack. When you need a new server, we deploy it in minutes with your exact configuration. No manual setup. No configuration drift. No forgotten steps at 11 PM.
More articles
How to Deploy BTCPay Server: Accept Bitcoin Payments with Zero Platform Fees
BTCPay Server lets you accept Bitcoin and Lightning payments with no platform fees. Only standard Bitcoin network fees apply. Here is how to deploy it with Docker Compose.
Self-Hosted Crypto Payment Processors: BTCPay Server, Bitcart, and SHKeeper Compared
Accept Bitcoin and cryptocurrency payments directly with no platform fees, no intermediaries, and no KYC. Here is how BTCPay Server, Bitcart, and SHKeeper compare.
How to Deploy SHKeeper: Accept Crypto Payments with WooCommerce in 30 Minutes
SHKeeper supports Bitcoin, Ethereum, USDT, USDC, and 19+ cryptocurrencies with a free WooCommerce plugin. Zero platform fees -- only standard network fees apply.