Skip to main content
  1. Linux Articles/
  2. Ubuntu 26.04 LTS Server/

Automating the Initial Setup of Ubuntu 26.04 LTS Server with cloud-init

Table of Contents

Automating the Initial Setup with cloud-init

cloud-init performs the initial setup of a server on its first boot: host name, users, SSH keys, networking and package installation. It is built into the Ubuntu cloud images and is how servers are prepared in cloud and virtual environments.

The manual equivalents are covered in the other articles, such as Changing the Hostname and Creating Users and Granting sudo Privileges. This article shows how to have the same settings applied at boot instead.

The examples were captured on Ubuntu 26.04 LTS Server with cloud-init 26.1.

The Configuration You Provide (user-data)

The configuration handed to cloud-init is called user-data and is written in YAML beginning with #cloud-config. The server used in this article was started with the following.

The user-data used in this article
#cloud-config
hostname: sv2
manage_etc_hosts: true
timezone: Asia/Tokyo
users:
  - name: kazulog
    groups: [sudo]
    shell: /bin/bash
    sudo: "ALL=(ALL) NOPASSWD:ALL"
    lock_passwd: false
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... kazulog@client
chpasswd:
  expire: false
  users:
    - name: kazulog
      password: kazulog
      type: text
package_update: true
packages:
  - git
  - nginx
write_files:
  - path: /etc/motd
    content: |
      このサーバーは cloud-init で構成されています。
  - path: /var/www/html/index.html
    content: "<h1>provisioned by cloud-init</h1>\n"
runcmd:
  - [systemctl, restart, nginx]
  - [sh, -c, "echo provisioned at $(date -Is) >> /var/log/provision.log"]

The main keys are:

KeyMeaning
hostnameThe host name. With manage_etc_hosts: true, /etc/hosts is updated as well
timezoneThe time zone
usersUsers to create. groups with sudo grants administrative commands, sudo is the sudoers rule, and ssh_authorized_keys holds the public keys
chpasswdSets passwords. Without expire: false the user must change the password at first login
package_updatetrue runs the equivalent of apt update
packagesPackages to install
write_filesFiles to create, given as path and content
runcmdCommands to run at the end. Written as lists, they run without a shell
When users is given, the image’s default user (ubuntu on the Ubuntu cloud images) is not created. Always provide either an SSH public key or a password, or you will end up with a server nobody can log in to.

How the user-data Reaches the Server

cloud-init looks in a fixed place for the user-data when the machine boots. That place is the data source, and it differs by environment.

EnvironmentHow you provide itData source
Clouds (AWS, GCP, Azure)Paste the #cloud-config into the “user data” field when creating the instanceThe cloud’s metadata service
Virtualisation platforms (Proxmox, VMware, CML)Paste it into the platform’s cloud-init form. The platform builds an ISO image and attaches it as a virtual CD-ROMNoCloud
Your own machineBuild the ISO image (a seed ISO) yourself with cloud-localds and boot it alongside the cloud imageNoCloud

The examples in this article use the third mechanism, which is why cloud-init query subplatform reports config-disk (/dev/sr0): the configuration came from that ISO image.

Trying It on Your Own Machine

Without a cloud or a virtualisation platform you need three things:

  1. An Ubuntu cloud image (a ready-made disk image, not the installer ISO)
  2. A seed ISO holding the user-data
  3. Virtualisation software to run them (qemu/KVM, libvirt and so on)

The seed ISO is built with cloud-localds from the cloud-image-utils package.

Command to install cloud-localds
sudo apt install -y cloud-image-utils
Example: cloud-localds is available
kazulog@sv1:~$ which cloud-localds qemu-system-x86_64
/usr/bin/cloud-localds
/usr/bin/qemu-system-x86_64

Write the user-data to a file and hand it to cloud-localds.

Command to build the seed ISO
cloud-localds [ISO] [USER-DATA]
FieldValue
[ISO]Name of the ISO image to create
[USER-DATA]The user-data file, starting with #cloud-config
Example: building the seed ISO
kazulog@sv1:~$ cat user-data.yaml | head -6
#cloud-config
hostname: web01
manage_etc_hosts: true
timezone: Asia/Tokyo
users:
  - name: kazulog
kazulog@sv1:~$ cloud-localds seed.iso user-data.yaml
kazulog@sv1:~$ ls -lh seed.iso
-rw-rw-r-- 1 kazulog kazulog 366K Sep 10 21:19 seed.iso

To pass network settings as well, name a network-config file with -N.

Including the network configuration
cloud-localds -N [NETWORK-CONFIG] [ISO] [USER-DATA]
Example: the contents of the ISO with a network configuration
kazulog@sv1:~$ cloud-localds -N network-config.yaml seed.iso user-data.yaml
kazulog@sv1:~$ sudo mkdir -p /mnt/seed && sudo mount -o loop,ro seed.iso /mnt/seed
kazulog@sv1:~$ ls -l /mnt/seed
total 0
-rw-rw-r-- 1 kazulog kazulog  33 Sep 10 21:20 meta-data
-rw-r--r-- 1 kazulog kazulog  48 Sep 10 21:20 network-config
-rw-r--r-- 1 kazulog kazulog 478 Sep 10 21:20 user-data
kazulog@sv1:~$ sudo umount /mnt/seed

The ISO holds user-data and meta-data, plus network-config when -N was given. cloud-localds writes the meta-data for you.

Now boot a virtual machine with the cloud image as one disk and the seed ISO as another. The Ubuntu 26.04 cloud image boots via UEFI, so point qemu at the OVMF firmware.

Command to fetch the cloud image
wget https://cloud-images.ubuntu.com/releases/26.04/release/ubuntu-26.04-server-cloudimg-amd64.img
Command to start the virtual machine (qemu)
qemu-system-x86_64 -enable-kvm -m 1024 -smp 1 -nographic \
  -drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd \
  -drive if=pflash,format=raw,file=OVMF_VARS.fd \
  -drive file=ubuntu-26.04-server-cloudimg-amd64.img,format=qcow2,if=virtio \
  -drive file=seed.iso,format=raw,if=virtio \
  -net nic -net user

Copy /usr/share/OVMF/OVMF_VARS_4M.fd to OVMF_VARS.fd first: it stores the UEFI settings and each virtual machine needs its own. Install the ovmf and qemu-system-x86 packages beforehand.

On boot, cloud-init reads the seed ISO and applies the user-data. The console output below shows that web01 from the user-data became the host name and that the seed ISO (/dev/vdb) was the data source.

Console output of the virtual machine
Ubuntu 26.04 LTS web01 ttyS0

web01 login: [  444.354111] sh[1072]: Completed socket interaction for boot stage config
[  462.016133] cloud-init[943]: Cloud-init v. 26.1-0ubuntu3~26.04.1 running 'modules:final' at Thu, 10 Sep 2026 12:57:00 +0000. Up 461.65 seconds.
Console output (cloud-init finishing, with the data source)
[  465.718178] cloud-init[943]: Cloud-init v. 26.1-0ubuntu3~26.04.1 finished at Thu, 10 Sep 2026 12:57:04 +0000. Datasource DataSourceNoCloud [seed=/dev/vdb].  Up 465.66 seconds
A cloud image that has been enlarged with qemu-img resize fails to boot as it is. The backup GPT header stays at its original position, so the /boot partition is not detected and the machine drops into emergency mode (confirmed in testing). To grow the disk, use a tool such as virt-resize that also fixes the partition table, or boot the image without enlarging it.

Confirming That the Settings Were Applied

After boot, cloud-init status reports the result. --long adds the details.

Command to check the result
cloud-init status --long
Example: checking the result
kazulog@sv2:~$ cloud-init --version
/usr/bin/cloud-init 26.1-0ubuntu3~26.04.1
kazulog@sv2:~$ cloud-init status --long
status: done
extended_status: done
boot_status_code: enabled-by-generator
last_update: Thu, 01 Jan 1970 00:00:27 +0000
detail: DataSourceNoCloud [seed=/dev/sr0]
errors: []
recoverable_errors: {}
kazulog@sv2:~$
FieldMeaning
statusdone, running, error or disabled
boot_status_codeWhether cloud-init is enabled
detailThe data source that was used (see below)
recoverable_errorsProblems that did not stop the run
When something went wrong but the run completed, status stays done while extended_status becomes degraded done, and the details appear under recoverable_errors. Plain cloud-init status does not show this, so use --long.

When a password was set, the second column of passwd -S shows P.

Example: confirming the password was set
kazulog@sv2:~$ sudo passwd -S kazulog
kazulog P 2026-09-10 0 99999 7 -1

Check the individual settings with their own commands.

Example: confirming the settings
kazulog@sv2:~$ hostname
sv2
kazulog@sv2:~$ id kazulog
uid=1000(kazulog) gid=1000(kazulog) groups=1000(kazulog),27(sudo)
kazulog@sv2:~$ cat /etc/motd
このサーバーは cloud-init で構成されています。
kazulog@sv2:~$ curl -s localhost | head -2
<h1>provisioned by cloud-init</h1>
kazulog@sv2:~$ cat /var/log/provision.log
provisioned at 2026-09-10T19:37:39+09:00
provisioned at 2026-09-10T19:42:01+09:00
kazulog@sv2:~$ dpkg -l git nginx | grep '^ii'
ii  git            1:2.53.0-1ubuntu1  amd64        fast, scalable, distributed revision control system
ii  nginx          1.28.3-2ubuntu1.10 amd64        small, powerful, scalable web/proxy server
kazulog@sv2:~$ timedatectl | head -3
               Local time: Thu 2026-09-10 19:42:26 JST
           Universal time: Thu 2026-09-10 10:42:26 UTC
                 RTC time: Thu 2026-09-10 10:42:26
kazulog@sv2:~$ ls -l ~/.ssh/authorized_keys
-rw------- 1 kazulog kazulog 110 Sep 10 19:41 /home/kazulog/.ssh/authorized_keys

The host name, the user and its groups, the files from write_files, the packages from packages, the result of runcmd, the time zone and the public key are all in place.

Checking the Data Source

The place cloud-init read the user-data from is called the data source. Clouds use a metadata service; virtual environments often use an ISO image (NoCloud).

Commands to check the data source
cloud-init query platform
cloud-init query subplatform
Example: checking the data source
kazulog@sv2:~$ cloud-init query platform
nocloud
kazulog@sv2:~$ cloud-init query subplatform
config-disk (/dev/sr0)
kazulog@sv2:~$

Here the nocloud data source read the configuration from /dev/sr0, a virtual CD-ROM.

The user-data that was actually read is kept in /var/lib/cloud/instance/user-data.txt, so you can tell later which configuration built the server.

Example: the user-data that was read
kazulog@sv2:~$ sudo head -11 /var/lib/cloud/instance/user-data.txt
#cloud-config
hostname: sv2
manage_etc_hosts: true
timezone: Asia/Tokyo
users:
  - name: kazulog
    groups: [sudo]
    shell: /bin/bash
    sudo: "ALL=(ALL) NOPASSWD:ALL"
    lock_passwd: false
    ssh_authorized_keys:
kazulog@sv2:~$

Timing and Logs

cloud-init analyze blame shows how long each step took, which helps when boot is slow.

Command to check the timing
sudo cloud-init analyze blame
Example: checking the timing
kazulog@sv2:~$ sudo cloud-init analyze blame | head -10
-- Boot Record 01 --
     07.24200s (modules-final/config-package_update_upgrade_install)
     00.75900s (modules-config/config-apt_configure)
     00.23800s (init-local/search-NoCloud)
     00.15500s (modules-final/config-keys_to_console)
     00.13400s (init-network/config-set_passwords)
     00.11000s (modules-final/config-scripts_user)
     00.10500s (init-network/config-growpart)
     00.10200s (init-network/config-ssh)
     00.07300s (modules-final/config-ssh_authkey_fingerprints)
kazulog@sv2:~$

Package installation (config-package_update_upgrade_install) dominates the total.

There are two log files.

FileContents
/var/log/cloud-init.logcloud-init’s own detailed log
/var/log/cloud-init-output.logOutput of the commands it ran, such as apt
Example: the log files
kazulog@sv2:~$ sudo ls -l /var/log/cloud-init.log /var/log/cloud-init-output.log
-rw-r----- 1 root   adm   4059 Sep 10 19:42 /var/log/cloud-init-output.log
-rw-r----- 1 syslog adm 109119 Sep 10 19:42 /var/log/cloud-init.log
kazulog@sv2:~$

Validating the Syntax

A mistake in the user-data leaves part of the configuration unapplied while the server still boots. Validate it with cloud-init schema before you use it.

Command to validate a file
sudo cloud-init schema --config-file [FILE]
FieldValue
[FILE]The cloud-config file to validate
Example: validating a file
kazulog@sv2:~$ sudo cloud-init schema --config-file /tmp/good.yaml
Valid schema /tmp/good.yaml
kazulog@sv2:~$ sudo cloud-init schema --config-file /tmp/bad.yaml
Invalid user-data /tmp/bad.yaml
Error: Cloud config schema errors: packages: 'git' is not of type 'array', runcmds: Additional properties are not allowed ('runcmds' was unexpected)

Error: Invalid schema: user-data
kazulog@sv2:~$

Errors are reported with the key and the reason. Above, packages was given a string where a list is required, and runcmd was misspelled as runcmds.

Use --system to validate the configuration that was actually applied.

Example: validating the applied configuration
kazulog@sv2:~$ sudo cloud-init schema --system | tail -3
2. network-config at /var/lib/cloud/instances/ubuntu/network-config.json:
  Valid schema network-config

Starting Over

cloud-init only runs its setup on the first boot. In a test environment you can clear its state and reboot to have it run as a first boot again.

Commands to clear the state and start over
sudo cloud-init clean --logs
sudo reboot
Example: clearing the state
kazulog@sv2:~$ sudo cloud-init clean --logs
kazulog@sv2:~$ ls /var/lib/cloud/
seed
kazulog@sv2:~$

After the reboot the user-data is read again and runcmd runs once more.

Example: after the reboot
kazulog@sv2:~$ uptime -p
up 0 minutes
kazulog@sv2:~$ cloud-init status
status: done
kazulog@sv2:~$ cat /var/log/provision.log
provisioned at 2026-09-10T19:37:39+09:00
provisioned at 2026-09-10T19:42:01+09:00
provisioned at 2026-09-10T19:43:35+09:00
kazulog@sv2:~$ cat /etc/motd
このサーバーは cloud-init で構成されています。

runcmd ran again and added a new line to provision.log.

Do not run cloud-init clean on a production server. Users and networking would be reconfigured, which can leave the machine in an unintended state.

Disabling cloud-init

To keep cloud-init from running on a server that is already built, create the marker file.

Command to disable cloud-init
sudo touch /etc/cloud/cloud-init.disabled
Example: disabling cloud-init
kazulog@sv2:~$ sudo touch /etc/cloud/cloud-init.disabled
kazulog@sv2:~$ ls -l /etc/cloud/cloud-init.disabled
-rw-r--r-- 1 root root 0 Sep 10 19:44 /etc/cloud/cloud-init.disabled
kazulog@sv2:~$

After the next boot the status is disabled and the user-data is not read.

Example: confirming it is disabled
kazulog@sv2:~$ cloud-init status --long
status: disabled
extended_status: disabled
boot_status_code: disabled-by-marker-file
detail: Cloud-init disabled by /etc/cloud/cloud-init.disabled
errors: []
recoverable_errors: {}
kazulog@sv2:~$

detail states why it was disabled. Delete the file to enable it again.

About the Network Configuration

Network settings are handed over separately as network-config. The format is the same as Netplan, and it is written out as /etc/netplan/50-cloud-init.yaml. It is covered in Configuring the Network on Ubuntu 26.04 LTS Server.

Test Environment and Session Logs

The examples were captured on Ubuntu 26.04 LTS Server (cloud-init 26.1) running on CML. The session log of each step can be downloaded below.

StepSession log
Building the seed ISO (cloud-localds)log
The ISO with a network configurationlog
Installing cloud-localds and qemulog
Console of the VM booted from the seed ISOlog
Result and data sourcelog
Timing and passwordlog
Confirming the applied settingslog
Log fileslog
Validating the syntaxlog
Clearing the state and rebootinglog
After the rebootlog
Disabling cloud-initlog
Confirming it is disabledlog

Reference

cloud-init documentation

Related articles

Ubuntu official pages