This guide shows how to perform Thistle OTA on a Raspberry Pi 5. Start with a quick file‑update path (no A/B). Optionally, pre‑partition a Raspberry Pi OS image to add a rootfsB
partition and pre‑enable headless SSH/Wi‑Fi before flashing.
Raspberry Pi 5 uses a Pi firmware boot chain loading from the FAT boot partition. For OTA A/B, set the bootloader in the Thistle config to RaspberryPi
. Depending on the distro, the boot partition may be mounted at /boot
or /boot/firmware
.
- Official imaging: Raspberry Pi Imager (“Use custom” supports local images)
- General microSD flashing workflow: Prepare microSD
Quick path: File updates (no A/B)
Use Thistle’s file‑update mode to ship signed files and app bundles without changing partitions.
- Download tools
VER=1.5.0
curl -LO https://downloads.thistle.tech/embedded-client/$VER/trh-$VER-x86_64-unknown-linux-musl.gz
gunzip trh-$VER-x86_64-unknown-linux-musl.gz
chmod +x trh-$VER-x86_64-unknown-linux-musl && ln -sf trh-$VER-x86_64-unknown-linux-musl trh
- Device (TUC on RPi5, aarch64):
# On the device (SSH or serial)
VER=1.5.0
curl -LO https://downloads.thistle.tech/embedded-client/$VER/tuc-$VER-aarch64-unknown-linux-musl.gz
gunzip tuc-$VER-aarch64-unknown-linux-musl.gz
chmod +x tuc-$VER-aarch64-unknown-linux-musl
sudo mv tuc-$VER-aarch64-unknown-linux-musl /usr/local/bin/tuc
- Initialize and release a simple file update on the workstation
export THISTLE_TOKEN=$(cat) # paste token, Enter, then Ctrl-D
./trh init --persist="/boot"
mkdir -p example && echo "hello from thistle" > example/app
./trh prepare --target=./example --file-base-path=/opt/example
./trh release
- Copy config to device and run client
# On workstation — change host/user to what you configured when imaging
scp config.json <user>@<rpi-host-or-ip>:/tmp/tuc-config.json
# On device
sudo mv /tmp/tuc-config.json /boot/tuc-config.json
sudo tuc -c /boot/tuc-config.json
That’s it! File updates are fetched, verified, and installed atomically. Continue below only if you need rootfs A/B for boot‑time rollback.
Optional: Headless image prep (Raspberry Pi OS Bookworm, 64‑bit)
Prepare a Raspberry Pi OS image before flashing: create rootfsB
, enable SSH, preconfigure Wi‑Fi, and set hostname. Docker is optional; any live Linux works.
1. Download the image
mkdir -p ~/rpi-img && cd ~/rpi-img
# Example: Raspberry Pi OS Lite (64‑bit) .img(.xz). Decompress if needed
# Use Raspberry Pi Imager or download a current Bookworm Lite image manually
# and set rpi5.img accordingly.
# e.g., with a downloaded file named 2025-05-13-raspios-bookworm-arm64-lite.img.xz
curl -LO https://downloads.raspberrypi.com/raspios_lite_arm64/images/2025-05-13/2025-05-13-raspios-bookworm-arm64-lite.img.xz
xz -d 2025-05-13-raspios-bookworm-arm64-lite.img.xz
mv 2025-05-13-raspios-bookworm-arm64-lite.img rpi5.img
2. Start a Linux environment in Docker (optional)
docker run --rm -it --privileged -v "$PWD":/work ubuntu:24.04 bash
Inside the container:
apt-get update
apt-get install -y parted e2fsprogs kpartx util-linux
cd /work
3. Expand image size & map partitions
truncate -s +8G rpi5.img # add 8 GiB free space
losetup -Pf --show rpi5.img # e.g. /dev/loop0
kpartx -av /dev/loop0 # maps /dev/mapper/loop0p1, loop0p2
4. Resize rootfs (p2) to 6 GiB (example)
e2fsck -f /dev/mapper/loop0p2
resize2fs /dev/mapper/loop0p2 5900M # adjust larger if your image requires more space
parted -s /dev/loop0 unit GiB resizepart 2 6
e2fsck -f /dev/mapper/loop0p2
resize2fs /dev/mapper/loop0p2
parted -s /dev/loop0 -a optimal mkpart primary ext4 6GiB 100%
kpartx -a /dev/loop0
mkfs.ext4 -F -L rootfsB /dev/mapper/loop0p3
6. Enable headless SSH + Wi‑Fi
Mount rootfs and boot partitions:
mkdir -p /mnt/root /mnt/boot
mount /dev/mapper/loop0p2 /mnt/root
mount /dev/mapper/loop0p1 /mnt/boot
a) Enable SSH at boot (RPi OS mechanism)
# An empty file named 'ssh' in the boot (FAT) partition enables SSH on first boot
: > /mnt/boot/ssh
cat > /mnt/boot/wpa_supplicant.conf <<'EOF'
country=US
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
ssid="YOUR_SSID"
psk="YOUR_PASSWORD"
}
EOF
c) (Optional) Pre‑create a user for headless login
On recent Raspberry Pi OS releases, a user is required. Either use Raspberry Pi Imager’s customization to set username/password, or add userconf.txt
to the boot partition with a SHA‑512 hashed password:
# Replace 'pi' and the hash accordingly. Generate hash with: echo 'mypassword' | openssl passwd -6 -stdin
echo 'pi:$6$REDACTED_HASH' > /mnt/boot/userconf.txt
d) Set hostname
echo "rpi5" | tee /mnt/root/etc/hostname >/dev/null
# Ensure /etc/hosts maps 127.0.1.1 to the hostname
sed -i 's/^127.0.1.1.*/127.0.1.1\trpi5/' /mnt/root/etc/hosts || echo "127.0.1.1\trpi5" >> /mnt/root/etc/hosts
Unmount and clean up:
umount /mnt/boot /mnt/root
kpartx -dv /dev/loop0
losetup -d /dev/loop0
exit
7. Flash to SD
- Using Raspberry Pi Imager: select “Use custom” and choose
rpi5.img
.
- Or on macOS:
diskutil list # locate your SD (e.g., /dev/disk3)
diskutil unmountDisk /dev/diskN # replace diskN with your SD identifier
sudo dd if=rpi5.img of=/dev/rdiskN bs=4m status=progress
sync
8. First boot
- Partitions:
- p1 → boot (FAT)
- p2 → rootfs (6 GiB ext4)
- p3 → rootfsB (ext4, label
rootfsB
)
- SSH enabled automatically if
ssh
file present
- Wi‑Fi auto‑connects if
wpa_supplicant.conf
was provided
- Hostname set to
rpi5
(unless changed)
Login:
ssh <user>@rpi5.local
# or: ssh <user>@<rpi-ip>
On the workstation and the device, download TRH/TUC as appropriate, then export your Project Access Token on the workstation.
# Workstation: TRH (prepare/release)
VER=1.5.0
curl -LO https://downloads.thistle.tech/embedded-client/$VER/trh-$VER-x86_64-unknown-linux-musl.gz
gunzip trh-$VER-x86_64-unknown-linux-musl.gz
chmod +x trh-$VER-x86_64-unknown-linux-musl && ln -sf trh-$VER-x86_64-unknown-linux-musl trh
# Device: TUC (install)
# Use the user/host you configured; example uses mDNS hostname
# Note: Use the aarch64 binary for 64‑bit images. If you chose a 32‑bit image,
# use the armv7 binary instead.
ssh <user>@rpi5.local 'VER=1.5.0; curl -LO https://downloads.thistle.tech/embedded-client/$VER/tuc-$VER-aarch64-unknown-linux-musl.gz && gunzip tuc-$VER-aarch64-unknown-linux-musl.gz && chmod +x tuc-$VER-aarch64-unknown-linux-musl && sudo mv tuc-$VER-aarch64-unknown-linux-musl /usr/local/bin/tuc && tuc --help | head -n 3'
# Workstation: Project token for TRH
export THISTLE_TOKEN=$(cat)
Initialize and Prepare Rootfs Release
./trh init --persist="/boot"
./trh prepare --target=myrootfs.img
./trh release
Device Configuration
Create tuc-config.json
(adjust paths if your boot is /boot/firmware
):
{
"name": "rpi5",
"persistent_directory": "/boot",
"public_keys": ["<YOUR_PUBLIC_KEY>"],
"bootloader": "RaspberryPi",
"part_a": "/dev/mmcblk0p2",
"part_b": "/dev/mmcblk0p3"
}
Copy configuration to the board and persist under /boot
:
scp tuc-config.json <user>@rpi5.local:/tmp/
ssh <user>@rpi5.local 'sudo cp /tmp/tuc-config.json /boot/tuc-config.json'
Run Update
ssh <user>@rpi5.local
sudo tuc -c /boot/tuc-config.json
The device installs to the inactive slot and reboots. After reboot, run the client again to latch‑in the update.
Notes and Tips
- If your distro mounts the boot partition at
/boot/firmware
, point persistent_directory
there.
- For headless provisioning on Bookworm, Raspberry Pi Imager customization is the simplest route (set SSH, Wi‑Fi, user, hostname). Manual
ssh
+ wpa_supplicant.conf
still works and is widely documented.
- For more background on headless setup nuances on Bookworm vs Bullseye, see community articles (e.g., first‑run mechanisms and
userconf.txt
).