Build flashable .img.xz images for Raspberry Pi from the official Pi OS Lite base image, with odios pre-installed via the existing Ansible playbook.
The builder runs on x86_64 Debian/Ubuntu (local machine or GitHub Actions runner). It uses loopback mount + chroot + qemu-user-static to provision the image.
| Architecture | Base image | Output |
|---|---|---|
| armhf (32-bit) | Raspberry Pi OS Lite (Trixie) armhf | odio-<version>-armhf.img.xz |
| arm64 (64-bit) | Raspberry Pi OS Lite (Trixie) arm64 | odio-<version>-arm64.img.xz |
# Debian/Ubuntu
sudo apt-get install -y qemu-user-static binfmt-support parted \
e2fsprogs xz-utils wget psmisc zerofree
# Fedora
sudo dnf install qemu-user-static parted e2fsprogs xz wget psmisc zerofreeUsage: sudo ./image-builder/build.sh <command> [options]
Commands:
build Build an image
clean Remove work directory and temp files
Build options:
--arch <armhf|arm64> Target architecture (required)
--version <version> odios version tag (required)
--workdir <path> Working directory (default: /tmp/odios-build)
--output <path> Output directory (default: ./output)
--keep Don't delete workdir after build (for debugging)
--skip-download Reuse previously downloaded base image from workdir
--skip-shrink Skip shrink step (faster builds for testing)
# Build a release image
sudo ./image-builder/build.sh build --arch arm64 --version 2026.3.0
# Build from a PR pre-release
sudo ./image-builder/build.sh build --arch armhf --version pr-5
# Fast iteration: keep workdir and skip shrink
sudo ./image-builder/build.sh build --arch arm64 --version 2026.3.0 --keep --skip-shrink
# Rebuild without re-downloading the base image
sudo ./image-builder/build.sh build --arch arm64 --version 2026.3.0 --skip-download
# Clean up
sudo ./image-builder/build.sh clean --workdir /tmp/odios-build- Download Pi OS Lite base image, verify SHA256
- Prepare -- copy, grow by 2G, losetup, resize partition
- Chroot -- mount filesystems, set up QEMU user-mode emulation
- Provision -- download odios release archive, run Ansible playbook (
install_mode=image) - Firstboot -- install
odios-firstboot.service(updates service names if hostname changes) - Upgrade --
apt-get upgradeto ship with latest packages - Verify -- check
init_resizeis present incmdline.txtfor first-boot auto-expand - Shrink --
resize2fs -M, truncate image to minimum size + 16 MiB margin - Compress --
xzthe final image - Manifest -- generate
.rpi-imager-manifestfor Pi Imager integration
The CI produces a combined odio.rpi-imager-manifest uploaded to each release. This manifest points Pi Imager directly to the image download URLs on GitHub, so there is no need to download images manually -- Pi Imager handles the download and flashing.
Cloud-init customization (hostname, SSH, WiFi, user creation) is fully supported via the manifest, as Pi OS Trixie includes cloud-init natively.
- Open Raspberry Pi Imager
- Go to Options app > Content Repository > Use custom URL
- Enter the manifest URL:
https://github.com/b0bbywan/odios/releases/latest/download/odio.rpi-imager-manifest - The available images appear in the OS list
- Configure hostname, SSH, WiFi, and user, then flash
User configuration: All odios services run under the odio system user. To connect via SSH, you can either:
- Create a separate user through Pi Imager's customization screen (recommended)
- Reuse the
odiouser by setting its password in Pi Imager
Pi Imager 2.0+ does not offer customization for images loaded via "Use custom" directly (missing metadata, rpi-imager#1302). Two alternatives:
cloud-init manually: Download and flash the .img.xz from the GitHub release, then mount the boot partition and create user-data and network-config files:
# user-data (on boot partition)
#cloud-config
hostname: myhost
users:
- name: myuser
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
lock_passwd: false
plain_text_passwd: changeme
ssh_pwauth: true# network-config (on boot partition)
network:
version: 2
wifis:
wlan0:
dhcp4: true
access-points:
"MySSID":
password: "MyPassword"Pi Imager CLI:
rpi-imager --cli \
--cloudinit-userdata user-data \
--cloudinit-networkconfig network-config \
odio-<version>-<arch>.img.xz /dev/sdXOn first boot:
- Partition auto-expands to fill the SD card (Pi OS
init_resize) - cloud-init applies user configuration (hostname, SSH, WiFi, user)
- odios-firstboot ensures the
odiouser has the required groups (audio,bluetooth,input, etc.) and updates service names if the hostname was changed - SSH host keys are regenerated
All services (MPD, Spotifyd, Shairport-sync, upmpdcli, etc.) run as the odio user via systemd user units.
Default hostname is odio. If changed via cloud-init or Pi Imager, the firstboot service automatically updates Bluetooth adapter name, Spotifyd device name, and upmpdcli friendly name to match.
Edit config.sh with new URLs and SHA256 checksums from:
- https://downloads.raspberrypi.com/raspios_lite_arm64/images/
- https://downloads.raspberrypi.com/raspios_lite_armhf/images/
The .sha256 file in each release directory contains the checksum.