Hands-off Ubuntu installs with autoinstall and PXE
If you build more than one identical machine, doing the installer by hand gets old fast. Ubuntu's autoinstall takes a YAML answer file and runs the whole installation with no keyboard: partitioning, user, packages, post-install commands, the lot. Pair it with a PXE boot so the machine pulls everything over the network, and a bare box becomes a finished install with one power-on. This is on Ubuntu 24.04.
The provisioning host
One Linux box hands out the boot files. It needs a web server to serve the ISO and the autoinstall file, DHCP/TFTP for PXE, and whois for mkpasswd (which you need to hash the install password):
apt update
apt install nginx-light dnsmasq whois syslinux-common
Clear the default nginx page and drop the ISO and your config in the web root. Download the live-server ISO and keep it where nginx can serve it:
rm /var/www/html/index.nginx-debian.html
wget -P /var/www/html https://releases.ubuntu.com/noble/ubuntu-24.04-live-server-amd64.iso
Give the provisioning NIC a static address on the isolated build network. This is one of the few places a private range is the point, as it is a self-contained provisioning LAN:
ip addr add 192.168.123.1/24 dev eth0
DHCP, TFTP and the boot menu
dnsmasq does DHCP and TFTP in one. Put this in /etc/dnsmasq.d/pxe.conf:
interface=eth0,lo
bind-interfaces
dhcp-range=eth0,192.168.123.100,192.168.123.200
dhcp-boot=pxelinux.0
enable-tftp
tftp-root=/srv/tftp
Restart it:
/etc/init.d/dnsmasq restart
Now the PXE menu. This is where the magic flags live: autoinstall switches the installer into unattended mode, and the two URLs tell it where to fetch the answer file and the install source from your web server. Put this in /srv/tftp/pxelinux.cfg/default:
DEFAULT install
LABEL install
KERNEL vmlinuz
INITRD initrd
APPEND root=/dev/ram0 ramdisk_size=1500000 ip=dhcp autoinstall cloud-config-url=http://192.168.123.1/autoinstall.yaml url=http://192.168.123.1/ubuntu-24.04-live-server-amd64.iso
Copy the kernel, initrd and the PXE loader module into place. Mount the ISO to get the kernel and initrd:
mount -r /var/www/html/ubuntu-24.04-live-server-amd64.iso /mnt
cp /mnt/casper/{vmlinuz,initrd} /srv/tftp/
cp /usr/lib/syslinux/modules/bios/ldlinux.c32 /srv/tftp/
Generate the password hash
Never put a plaintext password in the answer file, and never reuse a hash from someone else's example, including this one. Generate your own SHA-512 crypt hash:
mkpasswd --method=SHA-512
It prompts for the password and prints a hash that starts with $6$. That string goes into the identity block below.
The autoinstall file
This is /var/www/html/autoinstall.yaml. It installs the desktop packages, creates one admin user from your generated hash, uses a plain (non-LVM) partition layout, and does a pile of post-install tidy-up. Replace the username, hostname and password with your own:
#cloud-config
autoinstall:
version: 1
packages:
- ubuntu-desktop
identity:
realname: 'Kiosk Admin'
username: kioskadmin
password: '$6$REPLACE_WITH_YOUR_MKPASSWD_HASH'
hostname: kiosk
storage:
layout:
name: direct
early-commands:
- echo 'linux-generic-hwe-24.04' > /run/kernel-meta-package
late-commands:
# Enable the boot splash
- >-
curtin in-target -- sed -i /etc/default/grub -e
's/GRUB_CMDLINE_LINUX_DEFAULT=".*/GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"/'
- curtin in-target -- update-grub
# Let NetworkManager handle networking
- rm /target/etc/netplan/00-installer-config*yaml
- >-
printf "network:\n version: 2\n renderer: NetworkManager"
> /target/etc/netplan/01-network-manager-all.yaml
# Strip server-only packages not wanted on a desktop/kiosk image
- >-
curtin in-target -- apt-get remove -y
btrfs-progs cryptsetup* lvm2 xfsprogs
ubuntu-server ubuntu-server-minimal
byobu finalrd kpartx mdadm needrestart open-iscsi
sg3-utils ssh-import-id sssd thin-provisioning-tools tmux
sosreport open-vm-tools motd-news-config landscape-common
# cloud-init finishes some setup on first boot, so keep it
- curtin in-target -- apt-get install -y cloud-init
- curtin in-target -- apt-get autoremove -y
# Optional: run your own post-install script served from the same host
- wget -O /target/postinstall.sh http://192.168.123.1/postinstall.sh
- curtin in-target -- bash /postinstall.sh
- rm /target/postinstall.sh
A machine PXE-booted against this installs itself end to end and reboots into a finished system. The postinstall.sh hook is where I do the per-image work, installing the application, applying the lockdown, so the YAML stays about the OS and the script stays about the product.
At scale: MAAS
PXE plus a YAML file is perfect for a handful of machines. Once you are commissioning racks of them, Canonical's MAAS (Metal as a Service) is the grown-up version: it is PXE, DHCP, image management and an inventory with a web UI and an API, so machines are enlisted, commissioned and deployed on demand.
A single-node install is region + rack controller on one host, backed by PostgreSQL. Create the database (use your own credentials, not these):
CREATE USER "maas" WITH ENCRYPTED PASSWORD 'CHANGE_ME';
sudo -i -u postgres createdb -O "maas" maasdb
Initialise MAAS against it:
sudo maas init region+rack --database-uri "postgres://maas:CHANGE_ME@localhost/maasdb"
Then create the admin account and finish setup from the printed URL:
sudo maas createadmin
http://192.168.123.1:5240/MAAS
For a fleet of identical kiosks the autoinstall file is still doing the real work; MAAS just takes over the booting, imaging and tracking so you are not hand-editing a TFTP menu every time.