How to Build an Embedded Linux Image from Containers

What You’ll Build

A single flashable .wic image where every component — kernel, BSP, system services, applications — is a container, assembled at Yocto build time and ready to write to SD/eMMC.

After flashing, the device boots into Pantavisor with the entire composition in /trails/0/. Subsequent updates ship as OTA state revisions via PVR — see Update Embedded Linux Firmware OTA.

The Two-Phase Model

Phase Tool Output
Build — produce a flashable initial image Yocto + meta-pantavisor .wic
Maintain — update the running device PVR + Pantahub OTA state revisions

This guide covers the build phase. PVR cannot produce a flashable image; Yocto cannot OTA a running device. Each tool owns its phase.

Prerequisites

  • A Yocto host (Linux with build essentials) — or use Docker/Podman + KAS for a clean environment.
  • Hardware target supported by meta-pantavisor (Raspberry Pi, etc.) or your own MACHINE definition.

Step 1 — Get meta-pantavisor

git clone https://github.com/pantavisor/meta-pantavisor.git
cd meta-pantavisor

The layer ships KAS configs for common boards plus the pvroot-image, pvrexport, and container-pvrexport BitBake classes that do the heavy lifting.

KAS reads a YAML config and orchestrates the BitBake build. For a Raspberry Pi 4 starter image:

kas build kas/scarthgap.yaml:kas/machines/raspberrypi-armv8.yaml:kas/bsp-base.yaml

KAS pulls layer dependencies (poky, meta-openembedded, etc.), sets the MACHINE, and runs bitbake pantavisor-starter. The output lands in:

build/tmp-scarthgap/deploy/images/raspberrypi-armv8/pantavisor-starter-raspberrypi-armv8.wic

That .wic is the full bootable image — BSP + Pantavisor runtime + every container in PVROOT_CONTAINERS_CORE.

Step 3 — (Optional) Customize the Composition

The image recipe declares which containers get baked in:

SUMMARY = "Starter Image for Pantavisor"
LICENSE = "MIT"

inherit image pvroot-image

# Core containers (always present in initial state)
PVROOT_CONTAINERS_CORE ?= "pv-pvr-sdk pv-alpine-connman pvwificonnect"

# Optional containers bundled as factory packages for first-boot install
PVROOT_CONTAINERS ?= "myapp my-monitoring"

# Underlying BSP image
PVROOT_IMAGE_BSP ?= "core-image-minimal"

Each entry in PVROOT_CONTAINERS_CORE is a Yocto recipe that produces a .pvrexport.tgz. Three recipe sources are supported (mix freely):

From the GitLab Package Registry

Fastest. Pulls a pre-built .pvrexport.tgz:

SUMMARY = "Alpine Linux + ConnMan platform container"
LICENSE = "CLOSED"

inherit pvrexport

BB_STRICT_CHECKSUM = "0"
PVCONT_NAME = "os"

SRC_URI += "\
    https://gitlab.com/api/v4/projects/pantacor%2Fpv-platforms%2Falpine-connman/packages/generic/alpine-connman/${PV}/alpine-connman.${PV}.${DOCKER_ARCH}.tgz;name=os;subdir=${BPN}-${PV}/pvrrepo/.pvr \
    file://mdev.json \
"

Built from Scratch with Yocto

Full control over the container’s contents:

SUMMARY = "Pantavisor WiFi Connect container"
LICENSE = "MIT"

inherit core-image container-pvrexport

IMAGE_BASENAME = "pvwificonnect"
IMAGE_FSTYPES = "pvrexportit"

IMAGE_INSTALL += "busybox pvwificonnect-app"

SRC_URI += "file://args.json file://config.json"

PVR_APP_ADD_GROUP = "platform"

Wrapped from a Docker / OCI Image

Reuse upstream or third-party containers:

SUMMARY = "Alpine D-Bus container from Docker"
LICENSE = "CLOSED"

inherit pvrexport

BB_STRICT_CHECKSUM = "0"
PVR_DOCKER_REF = "asac/alpine-dbus:latest"

PVR_APP_ADD_EXTRA_ARGS += " \
    --volume /var/pvr-volume-boot:boot \
    --volume /var/pvr-volume-revision:revision \
    --volume /var/pvr-volume-permanent:permanent \
"

Step 4 — What pvroot-image Does at Build Time

When BitBake assembles the image, pvroot-image:

  1. Builds each container recipe (if not cached).
  2. Extracts the resulting .pvrexport.tgz artifacts.
  3. Deploys them into /trails/0/ of the rootfs via pvr deploy.
  4. Mixes in the BSP container (pantavisor-bsp) automatically.
  5. Generates device.json with groups, volumes, and disks.
  6. Wraps everything as a flashable .wic (or .pvrexport.tgz for OTA-only deployment).

Result: a .wic that boots straight into a fully-composed Pantavisor system on first power-on.

Step 5 — Flash

Use pvflasher (handles .wic, .bmap, and compressed images, verifies checksums, supports Linux/macOS/Windows):

# Install
curl -fsSL https://raw.githubusercontent.com/pantavisor/pvflasher/main/scripts/install.sh | bash

# List candidate disks
pvflasher list

# Flash
sudo pvflasher copy \
  build/tmp-scarthgap/deploy/images/raspberrypi-armv8/pantavisor-starter-raspberrypi-armv8.wic \
  /dev/sdX

Replace /dev/sdX with the actual target — flashing the wrong disk destroys data irreversibly.

What Happens on First Boot

  1. Bootloader hands off to the kernel from the BSP container.
  2. Pantavisor takes over as PID 1, mounts /trails/0/ as the active state.
  3. Each container in the composition starts according to its group and status_goal.
  4. The device registers with Pantahub (if configured) and becomes manageable via PVR.

Common Pitfalls

  • Forgetting layer dependenciesmeta-pantavisor needs poky and meta-openembedded layers in bblayers.conf. KAS handles this automatically; manual setups don’t.
  • Wrong MACHINE — match the KAS machine YAML to your hardware exactly. RPi 4 is raspberrypi-armv8, not raspberrypi4.
  • Confusing .pvrexport.tgz with .wic.pvrexport.tgz is a deploy bundle for OTA; .wic is the flashable initial image. Don’t try to flash a .pvrexport.tgz to bare media.
  • Treating PVR as a build toolpvr does not build the image. It manages the state of an already-running Pantavisor device.

Next Steps