This guide provides a headless method for flashing a Raspberry Pi OS onto a bootable device by running on another another connected device. This approach is especially useful when you don't have direct access to the bootable device from your main computer.
Introduction
The goal of this guide is to help users flash a Raspberry Pi OS to an SSD, SD card, or other bootable device in a headless environment. This method uses another Raspberry Pi to flash the device, configure essential settings, and ensure successful booting from the new OS.
There are certain limitations to consider, such as the inability to boot from a USB when an NVMe SSD is connected. Based on personal experience, you need to flash a microSD card first and boot from it to get the OS onto the SSD.
Additionally, some manual changes may be necessary after flashing to ensure bootability, as the CLI version of the Raspberry Pi Imager doesn’t include customized settings.
For a More Comprehensive Guide
For a more comprehensive guide on headless Raspberry Pi OS flashing, visit
Download and run the script provided, and be sure to read the README for detailed instructions.
Requirements
Before starting, ensure you have the following:
- A Raspberry Pi that you will use to flash the OS (with SSH access).
- A bootable device (e.g., NVMe SSD, SD card, or USB drive).
- Pre-downloaded Raspberry Pi OS image (e.g.,
raspios_lite_arm64_latest
). - mv raspios_lite_arm64_latest raspios_lite_arm64_latest.xz
- Network access to configure Wi-Fi settings on the bootable device.
- A basic understanding of using the terminal and SSH.
wget
https://downloads.raspberrypi.org/raspios_lite_arm64_latest
Steps to Flash Raspberry Pi OS
The following YAML-based Ansible playbook automates the process of flashing the Raspberry Pi OS onto a bootable device. The playbook also configures essential settings like SSH, Wi-Fi, and user credentials, ensuring that the newly flashed device is ready to use after boot. Make sure to customize the playbook before running it on your device.
Playbook for Flashing OS
---
# Run with sudo ansible-playbook notebook_setup.yml
- name: Set up Raspberry Pi bootable device
hosts: localhost
become: yes
vars:
bootable_device: /dev/nvme0n1 # Change this to your device path, e.g., /dev/sda, /dev/mmcblk0, etc.
os_image_path: /home/os_username/raspios_lite_arm64_latest.xz
wifi_ssid: "WIFI_SSID"
wifi_password: "WIFI_PASSWORD"
new_username: "NEW_USERNAME"
new_password: "NEW_PASSWORD"
country: "US"
perform_full_wipe: true
fixed_wait_time: 60 # Adjust waiting time if needed
vars_prompt:
- name: confirmation
prompt: "Are you sure you want to flash {{ bootable_device }}? All current data on this device will be lost. Type 'yes' to confirm, or 'no' to cancel."
private: false
default: ""
pre_tasks:
- name: Verify confirmation
fail:
msg: "Operation cancelled by the user. Playbook execution aborted."
when: confirmation != 'yes'
- name: Check if OS image file exists
stat:
path: "{{ os_image_path }}"
register: os_image_stat
- name: Fail if OS image doesn't exist
fail:
msg: "The specified OS image {{ os_image_path }} does not exist."
when: not os_image_stat.stat.exists
tasks:
- name: Ensure partitions are unmounted
mount:
path: "{{ item }}"
state: unmounted
loop:
- /mnt/boot
- /mnt/rootfs
ignore_errors: yes
- name: Clear partition table (optional full wipe)
command: "dd if=/dev/zero of={{ bootable_device }} bs=512 count=1"
when: perform_full_wipe | default(false)
- name: Install required packages
apt:
name:
- rpi-imager
- parted
state: present
update_cache: yes
- name: Check if bootable device exists
stat:
path: "{{ bootable_device }}"
register: bootable_device_stat
- name: Fail if bootable device doesn't exist
fail:
msg: "The specified bootable device {{ bootable_device }} does not exist."
when: not bootable_device_stat.stat.exists
- name: Determine partition suffix
set_fact:
partition_suffix: "{{ 'p' if '/dev/mmcblk' in bootable_device or '/dev/loop' in bootable_device or '/dev/nvme' in bootable_device else '' }}"
- name: Flash new OS to bootable device
command: >
rpi-imager --cli {{ os_image_path }} {{ bootable_device }} --enable-writing-system-drives
register: flash_result
failed_when: flash_result.rc != 0
changed_when: flash_result.rc == 0
- name: Wait for system to settle after flashing
pause:
seconds: "{{ fixed_wait_time }}" # Replaces the dynamic partition detection loop
- name: Inform kernel of partition table changes
command: "partprobe {{ bootable_device }}"
ignore_errors: yes
- name: Ensure partitions are available
wait_for:
path: "{{ bootable_device }}{{ partition_suffix }}{{ item }}"
state: present
timeout: 30
loop:
- "1"
- "2"
register: wait_result
ignore_errors: yes
- name: Check partition availability results
fail:
msg: "Partition {{ bootable_device }}{{ partition_suffix }}{{ item.item }} is not available after 30 seconds."
when: item.failed
loop: "{{ wait_result.results }}"
- name: Debug partition availability
debug:
msg: "Partition {{ bootable_device }}{{ partition_suffix }}{{ item.item }} status: {{ item.state }}"
loop: "{{ wait_result.results }}"
- name: Fail if partitions are not available
fail:
msg: >-
Partitions are not available after flashing. Please check the device and try again.
when: wait_result is failed
- name: Create mount points
file:
path: "{{ item }}"
state: directory
loop:
- /mnt/boot
- /mnt/rootfs
- name: Mount bootable device partitions
mount:
path: "{{ item.path }}"
src: "{{ item.src }}"
fstype: "{{ item.fstype }}"
state: mounted
loop:
- path: /mnt/boot
src: "{{ bootable_device }}{{ partition_suffix }}1"
fstype: vfat
- path: /mnt/rootfs
src: "{{ bootable_device }}{{ partition_suffix }}2"
fstype: ext4
# Remaining tasks are unchanged
- name: Set up Wi-Fi configuration
copy:
content: |
country="{{ country }}"
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
ssid="{{ wifi_ssid }}"
psk="{{ wifi_password }}"
}
dest: /mnt/boot/wpa_supplicant.conf
- name: Enable SSH
file:
path: /mnt/boot/ssh
state: touch
- name: Ensure SSH password authentication is enabled
lineinfile:
path: /mnt/rootfs/etc/ssh/sshd_config
regexp: "^#?PasswordAuthentication"
line: PasswordAuthentication yes
- name: Disable SSH root login
lineinfile:
path: /mnt/rootfs/etc/ssh/sshd_config
regexp: "^#?PermitRootLogin"
line: PermitRootLogin no
- name: Check if user exists
command: "chroot /mnt/rootfs /bin/bash -c 'id -u {{ new_username }}'"
register: user_exists
failed_when: false
changed_when: false
- name: Remove existing user if present
command: "chroot /mnt/rootfs /bin/bash -c 'userdel -r {{ new_username }}'"
when: user_exists.rc == 0
- name: Create user on the new system
shell: |
chroot /mnt/rootfs /bin/bash -c "
useradd -m -s /bin/bash {{ new_username }} &&
echo '{{ new_username }}:{{ new_password }}' | chpasswd &&
usermod -aG sudo {{ new_username }}
"
- name: Allow passwordless sudo for the new user
copy:
content: "{{ new_username }} ALL=(ALL) NOPASSWD:ALL"
dest: "/mnt/rootfs/etc/sudoers.d/{{ new_username }}"
mode: "0440"
- name: Enable SSH service on the new system
shell: |
chroot /mnt/rootfs /bin/bash -c "
systemctl enable ssh
"
- name: Configure network on the new system
copy:
content: |
allow-hotplug eth0
iface eth0 inet dhcp
allow-hotplug wlan0
iface wlan0 inet dhcp
wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf
dest: /mnt/rootfs/etc/network/interfaces
- name: Set hostname
copy:
content: raspberrypi
dest: /mnt/rootfs/etc/hostname
- name: Configure hosts file
copy:
content: |
127.0.0.1 localhost
127.0.1.1 raspberrypi
# The following lines are desirable for IPv6 capable hosts
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
dest: /mnt/rootfs/etc/hosts
- name: Set locale
copy:
content: en_US.UTF-8 UTF-8
dest: /mnt/rootfs/etc/locale.gen
- name: Generate locale
command: chroot /mnt/rootfs /bin/bash -c 'locale-gen'
- name: Set default locale
copy:
content: LANG=en_US.UTF-8
dest: /mnt/rootfs/etc/default/locale
- name: Set timezone to Eastern Time (US)
command: >-
chroot /mnt/rootfs /bin/bash -c 'ln -sf /usr/share/zoneinfo/America/New_York /etc/localtime'
- name: Unmount partitions
mount:
path: "{{ item }}"
state: unmounted
loop:
- /mnt/boot
- /mnt/rootfs
- name: Final sync before reboot
command: sync
- name: Notify about success
debug:
msg: >
Raspberry Pi bootable device created successfully with OS image {{ os_image_path }} on {{ bootable_device }}.
- name: Remind user to safely remove device
debug:
msg: "Setup complete. You can now safely remove {{ bootable_device }} and boot your Raspberry Pi."
Running the Ansible Playbook
After creating the YAML file with the provided playbook content, follow these steps to run it:
- Save the playbook: Save the YAML content to a file named
flash_raspberrypi.yml
on your Raspberry Pi. - Recommended: Update your RPi in order for installation to succeed:
sudo apt update && sudo apt upgrade -y
- Install Ansible: If you haven't already, install Ansible on your Raspberry Pi:
sudo apt update sudo apt install ansible
- Customize the playbook: Before running, make sure to customize the variables in the
vars
section of the playbook to match your specific requirements:bootable_device
: Set this to the correct device path (e.g.,/dev/sda
,/dev/nvme0n1
)os_image_path
: Ensure this points to the correct location of your Raspberry Pi OS imagewifi_ssid
andwifi_password
: Update these with your Wi-Fi credentialsnew_username
andnew_password
: Set these to your desired values for the new user
- Run the playbook: Execute the playbook using the following command:
You may need to run this withansible-playbook flash_raspberrypi.yml
sudo
if your user doesn't have the necessary permissions:sudo ansible-playbook flash_raspberrypi.yml
- Monitor the output: Ansible will display the progress of each task in the playbook. Watch for any errors or warnings.
- Verify completion: Once the playbook finishes, you should see a success message indicating that the bootable device setup is complete.
- Reboot: After the playbook has run successfully, reboot your Raspberry Pi and ensure it boots from the newly flashed device.
Connect to Your New System
When trying to SSH into your new system, you might get an SSH error due to the new OS. To fix this:
1. Remove the old SSH key:
ssh-keygen -R your_raspberry_pi_ip_address
Replace your_raspberry_pi_ip_address with your Raspberry Pi's actual IP address.
2. You should now be able to SSH into your Raspberry Pi using the username and password you set up earlier. After your first login, it's a good practice to update your system:
sudo apt update && sudo apt upgrade -y
Fix the locale
sudo locale-gen en_US.UTF-8
# Or for example another country:
# sudo locale-gen sv_SE.UTF-8
sudo update-locale LANG=en_US.UTF-8
sudo reboot
Conclusion
sudo locale-gen en_US.UTF-8
# Or for example another country:
# sudo locale-gen sv_SE.UTF-8
sudo update-locale LANG=en_US.UTF-8
sudo reboot
Conclusion
After following the steps outlined above, your Raspberry Pi OS should be successfully flashed to the bootable device, ready for use. Be sure to configure Wi-Fi settings and enable SSH for easier headless access in the future.