Raspberry Pi 5 setup with NVMe SSD and PoE+ HAT

These are my notes on setting up two Raspberry Pi 5s (Tempus and Ancilla) with NVMe SSDs and PoE+ HATs, booting from NVMe rather than SD card. The HAT in use is the UCTRONICS U6287 PoE M.2 HAT, which provides PoE+ power, an M.2 NVMe slot, and a heatsink/fan — all in one board. It connects via the Pi 5’s PCIe FFC connector and passes through 8 GPIO pins.

Hardware

  • Raspberry Pi 5
  • UCTRONICS U6287 PoE M.2 HAT (NVMe + PoE+)
  • Kingston 128GB M.2 NVMe SSD (2242 form factor)
  • SD card (temporary, for initial setup only)

Flash the SD card

Use Raspberry Pi Imager on any PC/Mac. Choose Raspberry Pi OS Lite (64-bit) — it’s under Raspberry Pi OS (other), not the top-level list. Before writing, open the customisation options and set:

  • Hostname (e.g. tempus)
  • Username and password
  • Enable SSH
  • Locale and keyboard layout

Write to the SD card. Note: the customisation settings carry over to the SD card image, but not to anything subsequently written to NVMe — you’ll need to reconfigure after moving to NVMe.

Boot from SD card and prepare the NVMe

Insert the SD card, attach the NVMe HAT with the SSD installed, and power on. SSH in once it’s up. First, install nvme-cli and do a sanity erase of the new drive:

sudo apt install nvme-cli
sudo nvme sanitize /dev/nvme0n1 --sanact=2
sudo nvme sanitize-log /dev/nvme0n1

Check the sanitize log output — SSTAT: 0x2 and SPROG: 32767 (0x7FFF) means complete and successful.

Write the OS to the NVMe

Download the current Raspberry Pi OS Lite image and write it directly to the NVMe:

wget https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2024-11-19/2024-11-19-raspios-bookworm-arm64-lite.img.xz

xz -dc 2024-11-19-raspios-bookworm-arm64-lite.img.xz | sudo dd of=/dev/nvme0n1 bs=4M status=progress

Check the current image filename at raspberrypi.com/software/operating-systems/ as it changes with each release.

Set NVMe as boot device

sudo raspi-config

Go to Advanced Options → Boot Order → NVMe/USB Boot. Then shut down, remove the SD card, and reboot — the Pi should come up from NVMe.

sudo shutdown -h now

First boot from NVMe — reconfigure

Since the NVMe has a fresh image, SSH won’t be enabled and the hostname will be default. Hook up a monitor and keyboard for the first boot, then run:

sudo raspi-config

Set:

  • System Options → Hostname
  • Interface Options → SSH → Enable
  • Localisation Options → Locale — set en_US.UTF-8 and make it system default, then reboot before setting keyboard or you’ll get locale errors
  • Localisation Options → Keyboard — after locale is set and rebooted

Disable WiFi and Bluetooth

These Pis are wired-only, so remove the wireless stack and disable the hardware:

sudo apt purge wpasupplicant wireless-tools rfkill
sudo apt autoremove

Add to /boot/firmware/config.txt:

dtoverlay=disable-wifi
dtoverlay=disable-bt

Stop NetworkManager from overwriting resolv.conf

NetworkManager will keep overwriting /etc/resolv.conf by default. To stop it:

sudo nano /etc/NetworkManager/NetworkManager.conf

Add under [main]:

dns=none
sudo systemctl restart NetworkManager

Now /etc/resolv.conf can be edited directly and will stay put.

Gotcha: cmdline.txt PARTUUID is unique per device

When setting up the second Pi (Ancilla), I made the mistake of copying cmdline.txt from Tempus. This overwrote Ancilla’s root partition PARTUUID with Tempus’s, causing a boot failure and drop to initramfs shell with:

ALERT! PARTUUID=2bcbfb2c-02 does not exist. Dropping to a shell!

The fix from the initramfs shell:

mount /dev/nvme0n1p2 /root
mount /dev/nvme0n1p1 /root/boot/firmware
mount --bind /dev /root/dev
mount --bind /proc /root/proc
mount --bind /sys /root/sys
chroot /root
update-initramfs -u

And edit /boot/firmware/cmdline.txt to put the correct root=/dev/nvme0n1p2 or the correct PARTUUID back. Never copy cmdline.txt between machines — everything else is fair game.