Command Line Install Headless Raspberry Pi OS (bookworm)
Here's how we do our Raspberry Pi builds on the geekfarm. Most of the instructions I see for building raspi images require using the Raspberry Pi Imager or some other image utility. These instructions are for folks like me who just want to edit the files and run the commands themselves. This stuff is pretty simple, but doesn't seem to be well documented. Having this information makes it really easy to incorporate these steps in your own automation.
Step 1. Download the OS
Check for the latest release here:

Currently the latest release is trixie, but the initialization stuff has changed in trixie, and I haven't figured out how to fully automate it yet. So, let's stick with bookworm for now.
# where my raspi images live
cd ~/projects/rpi/images
# download the image
# use arm64 for raspi 4, 5, and pi zero 2w
curl -O https://downloads.raspberrypi.com/raspios_oldstable_lite_arm64/images/raspios_oldstable_lite_arm64-2025-10-02/2025-10-01-raspios-bookworm-arm64-lite.img.xz
# verify the digest from the website
sha256sum 2025-10-01-raspios-bookworm-arm64-lite.img.xz
#
# expected output
#
# a65ebc80a792f0d04b720ec829dbf7b66b36f56310dbea34d462b19ba17b3687 2025-10-01-raspios-bookworm-arm64-lite.img.xz
#
# extract the image
xz -d 2025-10-01-raspios-bookworm-arm64-lite.img.xz
Step 2. Burn the image
Insert the SD Card and determine the device.
# check for sd card immediately after inserting
dmesg | tail
# [1148783.447015] scsi host0: usb-storage 1-2:1.0
# [1148784.450887] scsi 0:0:0:0: Direct-Access Kingston UHS-II SD Reader 0004 PQ: 0 ANSI: 6
# [1148784.747769] sd 0:0:0:0: [sda] 62333952 512-byte logical blocks: (31.9 GB/29.7 GiB)
# [1148784.748569] sd 0:0:0:0: [sda] Write Protect is off
# [1148784.748572] sd 0:0:0:0: [sda] Mode Sense: 21 00 00 00
# [1148784.749313] sd 0:0:0:0: [sda] Write cache: disabled, read cache: enabled, doesn't support DPO or FUA
# [1148784.754798] sda: sda1 sda2
# [1148784.755260] sd 0:0:0:0: [sda] Attached SCSI removable disk
# [1148784.759276] sd 0:0:0:0: Attached scsi generic sg0 type 0
# [1148785.178103] EXT4-fs (sda2): mounted filesystem 4d48c72f-b919-4823-96cc-04d8e6dcc211 r/w with ordered data mode. Quota mode: none.
# alternately check with fdisk
sudo /usr/sbin/fdisk -l
#
# example output
#
# ...snip...
#
# Disk /dev/sda: 29.72 GiB, 31914983424 bytes, 62333952 sectors
# Disk model: UHS-II SD Reader
# Units: sectors of 1 * 512 = 512 bytes
# Sector size (logical/physical): 512 bytes / 512 bytes
# I/O size (minimum/optimal): 512 bytes / 512 bytes
# Disklabel type: dos
# Disk identifier: 0xa6973e7c
#
# ...snip...
#
IMPORTANT: Make sure the disk is not mounted. If it is mounted, you won't see errors during the 'dd' operation, but then the filesystem will have issues. Mine auto-mounted, but it took a minute or so after I inserted it, so patience is recommended Note the lines in the output that match your device name (the one in the example is /dev/sda).
# check to see if the disk is mounted
df -h
#
# example output - note the lines matching the device (mine are /dev/sda)
#
# Filesystem Size Used Avail Use% Mounted on
# udev 3.9G 0 3.9G 0% /dev
# tmpfs 807M 6.6M 800M 1% /run
# /dev/mmcblk0p2 117G 51G 61G 46% /
# tmpfs 4.0G 528K 4.0G 1% /dev/shm
# tmpfs 5.0M 64K 5.0M 2% /run/lock
# /dev/mmcblk0p1 510M 77M 434M 16% /boot/firmware
# tmpfs 806M 192K 806M 1% /run/user/1000
# /dev/sda1 510M 95M 416M 19% /mnt/bootfs
# /dev/sda2 29G 1.9G 26G 7% /mnt/rootfs
# unmount any filesystems that are mounted
umount /mnt/bootfs
umount /mnt/rootfs
Now use the device path to burn the image:
# wipe existing filesystem, this is probably not required
sudo dd if=/dev/zero of=/dev/sda bs=512 count=1
#
# should run really quickly, expected output is something like this:
#
# 1+0 records in
# 1+0 records out
# 512 bytes copied, 0.0323709 s, 15.8 kB/s
#
# use the device path to burn the image with dd, show progress
sudo dd if=2025-10-01-raspios-bookworm-arm64-lite.img of=/dev/sda status=progress bs=4M
#
# there was a pause for a minute or so at the end
# after it stopped showing progress
#
# here was the output I saw:
#
# 2726297600 bytes (2.7 GB, 2.5 GiB) copied, 332 s, 8.2 MB/s
# 650+0 records in
# 650+0 records out
# 2726297600 bytes (2.7 GB, 2.5 GiB) copied, 475.029 s, 5.7 MB/s#
#
Step 3. Configure
After the image finishes writing, the filesystem should be automatically mounted. Now it's time for configuration.
# run the commands below as root
sudo su -
# set variables
export RPI_HOSTNAME=myhostname
export RPI_USER=myuser
export RPI_PASS=mypass
# you may need to unmount the filesystem as before
# remount the filesystems
mkdir -p /mnt/bootfs /mnt/rootfs
mount /dev/sda1 /mnt/bootfs
mount /dev/sda2 /mnt/rootfs
# enable ssh
touch /mnt/bootfs/ssh
# set the hostname
echo "${RPI_HOSTNAME}" > /mnt/rootfs/etc/hostname
# replace the default hostname in /etc/hosts
perl -pi -e"s|raspberrypi|${RPI_HOSTNAME}|g" /mnt/rootfs/etc/hosts
# set up timezone
rm -f /mnt/rootfs/etc/localtime
echo "America/Los_Angeles" >/mnt/rootfs/etc/timezone
# set up default user - make sure to copy all the quotes
echo "${RPI_PASS}" | openssl passwd -6 -stdin | awk "{print \"${RPI_USER}:\"\$1}" > /mnt/bootfs/userconf.txt
# use the uuid command to generate a new uuid for the network config
uuid
#
# here's my output that is used in the example below
#
# dd8ecdf6-ac94-11f0-91f3-fb01c27eb002
# create your wifi config file
vi /mnt/rootfs/etc/NetworkManager/system-connections/SSID.nmconnection
To generate the content for the file, replace a few things below:
- SSID-NAME-HERE in two places with your wifi access point name
- PASSWORD-HERE with your wifi password
- value of 'uuid' with the one you generated above
[connection]
id=SSID-NAME-HERE
uuid=dd8ecdf6-ac94-11f0-91f3-fb01c27eb002
type=wifi
autoconnect=true
[wifi]
mode=infrastructure
ssid=SSID-NAME-HERE
[wifi-security]
auth-alg=open
key-mgmt=wpa-psk
psk=PASSWORD-HERE
[ipv4]
method=auto
[ipv6]
method=auto
Now just fix the perms and unmount the filesystems:
# now fix the perms on the file
chmod -R 600 /mnt/rootfs/etc/NetworkManager/system-connections/SSID.nmconnection
chown -R root:root /mnt/rootfs/etc/NetworkManager/system-connections/SSID.nmconnection
# finally, unmount the filesystems
umount /mnt/bootfs
umount /mnt/rootfs
Step 4. Boot up and log in
Pop the sd card into the raspi, plug in the power, and wait for it to boot it up.
Give it a few minutes for the first boot since it may be doing things like generating SSH keys and rebooting after resizing the filesystem.
Ideally this process would be fully automated, but if you are relying on wifi, there are unfortunately a few things you need to do interactively So, if you only have a wifi connection, you'll need a keyboard and monitor for this next set of instructions.
# unblock wifi using rfkill
rfkill unblock wifi
# turn on wifi
nmcli radio wifi on
# set the wifi country
wpa_cli -i wlan0 set country US
# now verify you have an ip address
ip a
If you are using a wired connection, find the IP address and verify it's up.
# if you have DHCP / DNS set up, try pinging using the hostname
ping mhostname
# alternately try pinging using mDNS
ping myhostname.local
# you might find the ip address using your router or wifi access point
# if all else fails, hook up a monitor and watch for the ip address
Now you can log in over ssh. If you previously logged into this raspi over ssh from other hosts, you'll need to clear the old ssh keys from the known hosts file on those other hosts.
# remove entries from known hosts file
ssh-keygen -R ${HOSTNAME}
# copy your ssh keys for password-free login
ssh-copy-id ${HOSTNAME}
# now try to login
ssh ${HOSTNAME}
Step 5. Future work
There are still a few things on my list.
- fully automate wifi setup so it can be done headless
- default keyboard mapping
- pre-install my ssh key
