Headless Raspberry Pi Imaging Cloud-Init: Setup Guide

Headless Raspberry Pi Imaging Setup

Headless Raspberry Pi imaging cloud-init automates first-boot configuration with no monitor, keyboard, or manual steps required. You write user-data, meta-data, and network-config YAML files, place them in the correct partition, and cloud-init applies them during the first boot sequence. The result is a Pi that joins the network, creates users, installs packages, and runs any specified commands without interaction. This approach scales from a single Pi to a fleet of identical devices.

Last tested: Ubuntu Server 24.04 LTS for Raspberry Pi | December 9, 2025 | Raspberry Pi 4 Model B (4GB) | cloud-init 24.x

Key Takeaways

  • Ubuntu Server for Raspberry Pi has cloud-init built in and is the most reliable starting point. Raspberry Pi OS Bookworm supports cloud-init via sudo apt install cloud-init but requires additional configuration.
  • Cloud-init reads three files: user-data (users, packages, commands), meta-data (hostname, instance ID), and network-config (interfaces, Wi-Fi). All three must be present on the cidata partition or /boot/firmware/ depending on the OS.
  • YAML indentation errors silently skip configuration. Use spaces not tabs. Validate with yamllint before flashing.
  • On Bookworm, the boot partition is /boot/firmware/, not /boot/. The old ssh file and wpa_supplicant.conf methods do not apply to Bookworm.
  • Run sudo cloud-init clean before cloning an image for reuse. This removes cached state so cloud-init reruns on the next boot.
  • When a headless Pi does not appear on the network, check /var/log/cloud-init.log via serial console rather than guessing at the configuration.
Headless Raspberry Pi imaging cloud-init workflow diagram showing user-data and network-config files on SD card being processed by cloud-init on first boot to configure SSH, users, and networking automatically

Headless Raspberry Pi Imaging Cloud-Init: How It Works

A headless Pi setup has no display, keyboard, or mouse. All initial configuration is applied before or during the first boot. Cloud-init is a widely-used industry standard for this, originally designed for cloud VM provisioning, it works equally well on single-board computers. On boot, cloud-init reads the YAML configuration files, applies settings in a defined order, and exits. Subsequent boots are unaffected unless you explicitly reset the cloud-init state.

The practical advantage over manual setup or bash scripts is structure and portability. Cloud-init is well-documented, produces reliable logs, and the same configuration files work across Raspberry Pi OS, Ubuntu Server, and other cloud-init-compatible distributions. For a single Pi it saves 10 minutes of setup. For ten or more Pis it eliminates repeated manual steps entirely.

Cloud-init vs Raspberry Pi Imager advanced settings

CapabilityRaspberry Pi ImagerCloud-init
Set hostnameYesYes
Enable SSHYesYes
Configure Wi-FiYesYes
Add SSH keysYesYes
Install packagesNoYes
Run commands on bootNoYes
Write config filesNoYes
Scriptable / version-controllableLimitedYes
Works for fleet deploymentManual per deviceYes (shared config)

For a single Pi with basic SSH and Wi-Fi needs, Raspberry Pi Imager’s advanced settings are sufficient. Cloud-init becomes the better choice when you need packages installed, services configured, or the same configuration applied consistently across multiple devices.

Choosing the OS

Ubuntu Server (recommended for cloud-init)

Ubuntu Server 24.04 LTS for Raspberry Pi ships with cloud-init pre-installed and configured. The cidata partition is recognised on first boot and all three config files are read automatically. This is the most reliable starting point and the path described in most cloud-init documentation. Download from ubuntu.com/download/raspberry-pi.

Raspberry Pi OS Bookworm

Raspberry Pi OS Bookworm does not include cloud-init by default but supports it after installation. The Raspberry Pi Foundation now recommends using Imager advanced settings for basic headless setup. For cloud-init on Pi OS Bookworm, install it after first boot or use a pre-built image that includes it, then place config files in /boot/firmware/. Note that the old /boot/ path no longer applies on Bookworm. The boot partition is now mounted at /boot/firmware/.

Verify the image before flashing

# Verify downloaded image integrity
sha256sum ubuntu-24.04-preinstalled-server-arm64+raspi.img.xz

# Compare with the checksum published on the download page

Step 1: Flash the Image

Flash the image using Raspberry Pi Imager or dd. Do not use Imager’s advanced settings when using cloud-init. The settings would conflict. Write the raw image and configure everything through the cloud-init files instead.

# Linux/macOS with dd (replace sdX with your actual device -- verify with lsblk first)
xzcat ubuntu-24.04-preinstalled-server-arm64+raspi.img.xz | sudo dd of=/dev/sdX bs=4M status=progress conv=fsync

# Verify partition structure after writing
lsblk /dev/sdX

After flashing, the SD card has two partitions: a FAT32 boot partition labelled system-boot and an ext4 root partition. Mount the boot partition to place the cloud-init files.

Expected result: lsblk shows two partitions. The boot partition mounts without errors and contains user-data, meta-data, and network-config placeholder files (Ubuntu images ship with empty versions of these).

Step 2: Write the Cloud-Init Configuration Files

meta-data

The meta-data file sets the instance identity. Keep it minimal:

instance-id: pi-001
local-hostname: pi-headless

user-data

The user-data file must start with #cloud-config on the first line. Without this, cloud-init ignores the file entirely.

#cloud-config

hostname: pi-headless
manage_etc_hosts: true

users:
  - name: pi
    sudo: ALL=(ALL) NOPASSWD:ALL
    groups: users, admin, sudo
    shell: /bin/bash
    lock_passwd: false
    ssh_authorized_keys:
      - ssh-rsa AAAAB3NzaC1yc2E... your-public-key-here

ssh_pwauth: false

package_update: true
package_upgrade: true

packages:
  - htop
  - git
  - ufw

runcmd:
  - ufw allow 22/tcp
  - ufw --force enable
  - echo "cloud-init setup complete" >> /var/log/setup.log

network-config

For Ethernet with DHCP:

version: 2
ethernets:
  eth0:
    dhcp4: true
    optional: true

For Wi-Fi with DHCP:

version: 2
wifis:
  wlan0:
    dhcp4: true
    optional: true
    access-points:
      "YourSSID":
        password: "YourPassword"

For a static IP:

version: 2
ethernets:
  eth0:
    addresses:
      - 192.168.1.50/24
    routes:
      - to: default
        via: 192.168.1.1
    nameservers:
      addresses: [1.1.1.1, 8.8.8.8]

YAML validation before flashing

sudo apt install yamllint -y
yamllint user-data
yamllint network-config

Fix any indentation or syntax errors before placing the files on the SD card. A YAML error in user-data causes cloud-init to skip that file silently. The Pi boots normally but none of the configuration is applied.

Expected result: yamllint reports no errors on both files. File names are exactly user-data, meta-data, and network-config with no extensions.

Step 3: Place Files on the SD Card

For Ubuntu Server images, replace the placeholder files in the system-boot partition (the FAT32 partition visible when the SD card is inserted on any OS):

# Mount the boot partition (adjust path as needed)
sudo mount /dev/sdX1 /mnt

# Replace the placeholder files
sudo cp user-data /mnt/user-data
sudo cp meta-data /mnt/meta-data
sudo cp network-config /mnt/network-config

sudo umount /mnt

For Raspberry Pi OS Bookworm with cloud-init installed, place the files in /boot/firmware/. The cidata partition approach (a separate FAT partition labelled cidata with just the three files) also works with both OS options and does not require modifying the existing partitions.

File placement rules:

  • Filenames are case-sensitive. user-data not User-Data.
  • No file extensions. user-data not user-data.yaml.
  • No BOM (Byte Order Mark). Save files as UTF-8 without BOM, especially if editing on Windows.

Expected result: The boot partition contains user-data, meta-data, and network-config with the correct content. Eject the card safely before inserting into the Pi. For long-term reliability, running the Pi from a USB SSD rather than microSD is recommended. See Booting Raspberry Pi from USB SSD and Preventing SD Card Corruption on Raspberry Pi.

Step 4: First Boot and Verification

Find the Pi on the network

# Try mDNS hostname first
ping pi-headless.local

# Or scan the subnet for new devices
nmap -sn 192.168.1.0/24 | grep -A2 "Raspberry\|ubuntu"

# Or check your router's DHCP lease table

Connect via SSH

ssh pi@pi-headless.local
# or
ssh pi@192.168.1.x

Check cloud-init status

# Wait for cloud-init to complete if it is still running
cloud-init status --wait

# View the full log
sudo less /var/log/cloud-init.log

# Check for errors specifically
sudo grep -i "error\|warn\|fail" /var/log/cloud-init.log

Expected result: cloud-init status returns done. SSH connects using the key specified in user-data. The packages listed in packages: are installed. /var/log/setup.log contains the completion message from runcmd.

Automating Boot Tasks

bootcmd vs runcmd

bootcmd runs very early in the boot process, before most services are available. Use it only for tasks that genuinely need to happen before networking or user space is fully up. runcmd runs later and is where most setup tasks belong: package installs, service configuration, and file manipulation.

#cloud-config
runcmd:
  - apt-get install -y docker.io
  - systemctl enable --now docker
  - usermod -aG docker pi
  - curl -fsSL https://tailscale.com/install.sh | sh

Writing files on first boot

#cloud-config
write_files:
  - path: /etc/motd
    content: |
      Headless Pi - provisioned by cloud-init
  - path: /home/pi/.bashrc
    append: true
    content: |
      alias ll='ls -alF'

Cloning and Fleet Deployment

Clone an SD card

# Create image from configured SD card
sudo dd if=/dev/sdX of=pi-base.img bs=4M status=progress

# Flash to multiple cards
sudo dd if=pi-base.img of=/dev/sdY bs=4M status=progress conv=fsync

Reset cloud-init state before cloning

# Run on the Pi before cloning
sudo cloud-init clean
sudo cloud-init clean --logs  # also clears logs for a truly fresh start
sudo shutdown -h now

Without running cloud-init clean, cloned images will not rerun cloud-init on first boot because the cached run state indicates the configuration has already been applied.

Unique hostnames and SSH keys per device

Cloned images share the same hostname and SSH host keys by default. For a fleet, generate unique hostnames dynamically based on the MAC address and regenerate SSH host keys on first boot:

#cloud-config
runcmd:
  # Set hostname based on MAC address last 4 chars
  - SUFFIX=$(cat /sys/class/net/eth0/address | tr -d ':' | tail -c 5)
  - hostnamectl set-hostname "pi-${SUFFIX}"
  # Regenerate SSH host keys
  - rm -f /etc/ssh/ssh_host_*
  - dpkg-reconfigure openssh-server

Troubleshooting

Pi does not appear on the network

If the Pi does not appear after 2 to 3 minutes, connect via serial console using a USB-to-TTL cable wired to the Pi’s GPIO UART pins (TX, RX, GND). Use screen or minicom at 115200 baud to get a console:

screen /dev/ttyUSB0 115200

From the serial console, check the cloud-init log and network status:

sudo cat /var/log/cloud-init.log | tail -50
ip addr show
systemctl status systemd-networkd

Cloud-init ran but configuration was not applied

Check that user-data starts with #cloud-config on the very first line with no blank line or BOM before it. Validate the YAML with yamllint. Check the cloud-init log for the specific module that failed. If cloud-init ran previously (cached state), run sudo cloud-init clean and reboot.

Wi-Fi not connecting

Check SSID and password accuracy in network-config. Confirm the indentation is correct. The password field must be indented under the SSID entry. Check that the Wi-Fi interface name matches your hardware (wlan0 on most Pi models, but verify with ip a). Confirm the country code is set correctly in /boot/firmware/config.txt on Raspberry Pi OS:

# Add to /boot/firmware/config.txt on Pi OS if Wi-Fi is not initialising
country=US

SSH access denied after boot

Confirm the public key in user-data matches the private key you are using to connect. SSH public keys must be on a single line with no line breaks. Check that the username in users: matches the username in your SSH command. If ssh_pwauth: false is set, password login is disabled and key auth is required.

FAQ

Do I need cloud-init for every headless Pi setup?

No. For a single Pi with basic SSH and Wi-Fi needs, Raspberry Pi Imager’s advanced settings handle the configuration without cloud-init. Cloud-init becomes useful when you need packages installed on first boot, services configured, or the same configuration deployed consistently to multiple devices.

Can I use cloud-init with Raspberry Pi OS?

Yes, but with more setup than Ubuntu Server. Install cloud-init with sudo apt install cloud-init, then place the config files in /boot/firmware/. The cidata partition approach also works. Ubuntu Server for Raspberry Pi is the simpler path if cloud-init is the primary reason for choosing the OS.

Why is SSH not working after first boot?

The most common causes are: SSH keys not configured in user-data, YAML syntax error in user-data that prevented the file from being processed, Wi-Fi not connecting so the Pi has no network address, or the wrong username in the SSH command. Check /var/log/cloud-init.log via serial console to identify which step failed.

How do I debug a cloud-init failure?

Connect via serial console and read /var/log/cloud-init.log. The log shows which modules ran, which succeeded, and which failed with error messages. journalctl -u cloud-init provides the same information through systemd. For pre-boot issues, add a line to bootcmd that writes to a file. If the file exists after boot, cloud-init reached that stage.

Is cloud-init secure for production use?

Yes with proper configuration. Use SSH keys rather than passwords, set ssh_pwauth: false, and do not store credentials in plain text in user-data. Store user-data files in version control but exclude any secrets using environment variable substitution or a secrets manager. Rotate SSH keys and credentials after initial provisioning if the user-data file was accessible to others during deployment.

References


About the Author

Chuck Wilson has been programming and building with computers since the Tandy 1000 era. His professional background includes CAD drafting, manufacturing line programming, and custom computer design. He runs PidiyLab in retirement, documenting Raspberry Pi and homelab projects that he actually deploys and maintains on real hardware. Every article on this site reflects hands-on testing on specific hardware and OS versions, not theoretical walkthroughs.

Last tested hardware: Raspberry Pi 4 Model B (4GB). Last tested OS: Ubuntu Server 24.04 LTS for Raspberry Pi. cloud-init 24.x.