Building a Custom Embedded Linux Image for the Raspberry Pi 5 with Yocto

If you’ve ever wanted to move beyond Raspberry Pi OS and build a purpose-built, minimal Linux distribution for your Raspberry Pi 5, the Yocto Project is the tool for the job. Yocto lets you construct a custom embedded Linux image — down to the kernel config, init system, and package set — instead of dragging along a general-purpose desktop distro.

This post walks through a real, end-to-end session: cloning Yocto, adding Raspberry Pi support, building core-image-base, flashing it to an SD card, and then layering on extras like Docker, U-Boot, Wi-Fi via ConnMan, and a local Debian package repository. It’s written the way it actually happened, warts and all, so you can follow along and adapt it to your own board.

Prerequisites

You’ll need a Linux build host (or WSL2) with the usual Yocto build dependencies installed, a Raspberry Pi 5, an SD card (or USB/NVMe target), and a fair amount of disk space and patience — a full Yocto build can easily take an hour or more the first time through.

1. Cloning Poky and the Raspberry Pi Layer

Everything starts with Poky, the reference distribution that Yocto ships. We’ll use the scarthgap branch, which is a Long Term Support release maintained until 2028 — a solid choice if you don’t want to be re-porting your BSP every six months.

git clone -b scarthgap https://github.com/yoctoproject/poky poky-rpi
cd poky-rpi
git clone -b scarthgap https://github.com/agherzan/meta-raspberrypi.git

meta-raspberrypi is the board support package (BSP) layer that teaches Yocto how to build images for Raspberry Pi hardware — boot firmware, GPU drivers, device trees, and machine definitions all live here.

2. Setting Up the Build Environment

Next, source the environment setup script. This drops you into a build/ directory with a conf/ folder containing your layer and local configuration.

source oe-init-build-env

Add the Raspberry Pi layer to your build:

bitbake-layers add-layer ../meta-raspberrypi/

You can always sanity-check which layers are active:

cat conf/bblayers.conf

3. Configuring the Build for Raspberry Pi 5

Open conf/local.conf and set the machine and a few key variables:

vim conf/local.conf
MACHINE = "raspberrypi5"
INIT_MANAGER = "systemd"
LICENSE_FLAGS_ACCEPTED = "synaptics-killswitch"
  • MACHINE tells Yocto which BSP configuration to build against — here, the Raspberry Pi 5.
  • INIT_MANAGER switches the init system to systemd, which is generally preferred over SysVinit for modern embedded targets.
  • LICENSE_FLAGS_ACCEPTED whitelists a proprietary or restricted-license component (in this case, a Synaptics touchpad killswitch driver) that Yocto would otherwise refuse to build without explicit consent.

4. Building the Image

With the configuration in place, kick off the build:

bitbake core-image-base

core-image-base is a small, console-only reference image — a good starting point before you start layering on graphics stacks, containers, or your own applications.

A Kernel Quirk Worth Knowing About

On some recent kernels, AppArmor restricts unprivileged user namespaces by default, which can interfere with certain container or sandboxing workflows. If you hit this, you can relax it via a sysctl setting:

vim /etc/sysctl.d/60-apparmor-namespace.conf
kernel.apparmor_restrict_unprivileged_users=0

Then reboot the build host for it to take effect.

5. Flashing the Image to an SD Card

Once the build finishes, your output image lands in the deploy directory:

tmp/deploy/images/raspberrypi5/core-image-base-raspberrypi5.rootfs.wic.bz2

Yocto images are compressed .wic files — a full disk image format that includes partition tables, bootloader, and rootfs. You can write it directly to a block device with dd:

bzcat tmp/deploy/images/raspberrypi5/core-image-base-raspberrypi5.rootfs.wic.bz2 | sudo dd of=/dev/sdd

Double-check your device node before running this — dd will happily overwrite the wrong disk with no warning.

A faster alternative for repeated flashing is bmaptool, which only writes the blocks that actually contain data instead of streaming the entire image:

bmaptool copy /tmp/deploy/images/raspberrypi5/core-image-base-raspberrypi5.rootfs.wic.bz2 /dev/sdd

6. First Boot

Pop the card into the Pi 5 and power it on. core-image-base ships without a password on the root account by default, so you can log in immediately:

login: root
Password: (press Enter)

From here it’s a bare console — no SSH, no Wi-Fi, nothing extra. That’s the point: you decide what goes in.

7. Exploring Your Build with BitBake Tools

Back on the build host, a few commands are worth keeping in your toolbox:

bitbake-getvar EXTRA_IMAGE_FEATURES

bitbake-getvar prints the final, resolved value of any BitBake variable — extremely useful when a variable is being set (or overridden) across multiple layers and you’re not sure which value actually wins.

To visualize task dependencies for a recipe, taskexp gives you an interactive, ncurses-based dependency explorer:

bitbake -g core-image-minimal -u taskexp

And to search the source tree for recipes matching a keyword — say, all the X11-related image recipes:

find . -path '*/images/*' | grep x11

8. Adding More Layers: Containers, OpenEmbedded, and U-Boot

A minimal image only gets you so far. Real projects usually pull in additional community layers. Two of the most common are meta-openembedded (which itself bundles meta-oe, meta-python, meta-networking, and meta-filesystems) and meta-virtualization (for Docker/container support):

bitbake-layers add-layer ../meta-openembedded/meta-oe
bitbake-layers add-layer ../meta-openembedded/meta-python
bitbake-layers add-layer ../meta-openembedded/meta-networking
bitbake-layers add-layer ../meta-openembedded/meta-filesystems
bitbake-layers add-layer ../meta-virtualization

To pull Docker (and networking tools) into the image itself, append to local.conf:

IMAGE_INSTALL:append = " docker-moby connman connman-client"

Once built and deployed, you can validate the Docker runtime on-target:

docker run -it hello-world
docker run -it --expose 80 -p 80:80 wordpress

Yes — you can run WordPress itself on a Yocto-built Raspberry Pi 5. It’s a fun way to prove the container stack works end-to-end.

Switching the Bootloader to U-Boot

By default, the Raspberry Pi BSP uses the stock Broadcom firmware bootloader. If you want a more flexible, scriptable bootloader — useful for A/B updates or custom boot logic — you can bring in U-Boot via the LTS mixins layer:

git clone -b scarthgap/u-boot https://git.yoctoproject.org/meta-lts-mixins
bitbake-layers add-layer ../meta-lts-mixins/

Then enable it in local.conf:

RPI_USE_U_BOOT = "1"

At boot, press any key to interrupt autoboot and drop into the U-Boot shell, where you can inspect and manipulate the boot environment:

printenv
env set test 1
printenv test
saveenv

On the running system, confirm what you booted:

cat /etc/os-release
uname -a

For managing the U-Boot environment from within Linux (rather than at the boot prompt), the u-boot-fw-utils package provides fw_printenv and fw_setenv, configured via /etc/fw_env.config. You can also inspect the raw environment blob directly:

cat /boot/uboot.env

9. Getting Wi-Fi Working with ConnMan

For network management on embedded images, ConnMan is a lightweight alternative to NetworkManager. First bring up the wireless interface:

ip a
rfkill unblock wifi
ip link set wlan0 up

Then drive ConnMan interactively:

connmanctl

Inside the ConnMan shell:

technologies
enable wifi
scan wifi
services
agent on
connect wifi_<ssid-hash>

You’ll be prompted for the network passphrase interactively via the agent.

If you’d rather configure networking declaratively (and skip ConnMan for wired/simple Wi-Fi setups), systemd-networkd works too:

vim /etc/systemd/network/10-wlan.network
[Match]
Name=wlan0

[Network]
DHCP=yes

10. Setting Up a Local Package Repository

For over-the-air style workflows or just faster iteration without full image rebuilds, Yocto can generate installable packages rather than baking everything into the rootfs. Yocto supports several package formats — ipk, rpm, deb, and plain tar — controlled by:

bitbake-getvar PACKAGE_CLASSES

Build an individual package and generate a package index:

bitbake nano
bitbake package-index

Then serve the package feed over HTTP from the build host:

cd tmp/deploy/deb
busybox httpd -p 8000 -f -v

On the Raspberry Pi target, point apt at your local feed:

vim /etc/apt/sources.list.d/local-repo.list
deb [trusted=yes] http://192.168.1.15:8000/all ./
deb [trusted=yes] http://192.168.1.15:8000/cortexa76 ./
deb [trusted=yes] http://192.168.1.15:8000/raspberrypi5 ./

Then install as usual:

apt update
apt install nano

Or, for a single .deb file without a repo:

dpkg -i nano_<version>.deb

For production fleets, this same package-based approach pairs naturally with an update mechanism like Mender, RAUC, or SWUpdate — worth investigating once you’re past the prototyping stage.

11. Creating Your Own Layer

Sooner or later you’ll want to add your own recipes rather than editing upstream layers. BitBake makes scaffolding a new layer trivial:

bitbake-layers create-layer ../meta-navis
bitbake-layers add-layer ../meta-navis

This generates a standard layer skeleton:

../meta-navis/
├── conf
│   └── layer.conf
├── COPYING.MIT
├── README
└── recipes-example
    └── example
        └── example_0.1.bb

From here, you can add your own recipes-* directories for application code, custom configuration files, systemd units, or anything else specific to your product.

Wrapping Up

Starting from nothing more than git clone and bitbake core-image-base, we ended up with a fully custom Raspberry Pi 5 image featuring systemd, Docker support, a U-Boot-based boot flow, Wi-Fi via ConnMan, and a local Debian-style package feed for fast iteration — plus our own layer ready for real application recipes.

That’s the real power of Yocto: every one of these pieces is optional and composable. You only pay (in build time and image size) for what you actually include. If you’re building a product around the Raspberry Pi 5, this is the kind of foundation that scales from a prototype on your desk to a fleet running your application in the field.