Skip to main content

IoT gateway with composable containers

What an IoT gateway needs

A typical IoT or industrial gateway sits at the boundary between field-side equipment (sensors, PLCs, machines) and the cloud. It needs:

  • a stable BSP for the chosen hardware (RPi, NXP, TI, etc.),
  • networking (Ethernet, Wi-Fi, LTE, sometimes LoRa),
  • secure remote access (VPN, mesh networking),
  • protocol adapters (Modbus, OPC-UA, CAN, BACnet, custom serial),
  • a local broker / buffer (MQTT, store-and-forward),
  • edge processing (filtering, aggregation, anomaly detection),
  • OTA updates for every layer above,
  • auditable, recoverable updates for compliance and uptime.

Building this as a monolithic image means every change to one piece reflashes the whole device. Composable containers fix that.

Pantavisor's gateway composition

Each gateway is a state composed of independent containers:

my-iot-gateway/ (state JSON)
├── bsp/ # Kernel, modules, firmware (e.g. RPi 4 ARMv8)
├── os/ # Alpine + ConnMan (networking)
├── tailscale/ # Mesh VPN container
├── modbus/ # Industrial protocol adapter
├── mqtt-broker/ # Local MQTT broker (Mosquitto)
├── edge-app/ # Custom edge processing
└── pvr-sdk/ # Optional management container (dev/diagnostics)

Each container is built and versioned independently, has its own run.json and _config/<container>/ overlays, declares dependencies on other containers via the pv-xconnect service mesh, and has its own status_goal so failures are caught and rolled back automatically.

Why composability matters here

  • Different teams, different cadences. The kernel team ships a quarterly BSP update; the networking team patches ConnMan when a CVE drops; the edge-app team ships weekly. With monolithic firmware every cadence collides. With containers, each team owns its piece and ships independently.
  • Different SKUs, shared base. A "Wi-Fi-only" and a "Wi-Fi + LTE" gateway can share BSP, OS, MQTT, and edge-app, differing only in the modem container. No fork, no parallel build pipelines.
  • Per-customer customization. Customer A wants Modbus + S3 export; customer B wants OPC-UA + Azure IoT Hub. Two custom containers per customer, everything else shared.

Building one — concrete example

Using the Yocto path (see Build with Yocto), compose the initial image from container recipes:

SUMMARY = "Industrial IoT Gateway"
LICENSE = "MIT"

inherit image pvroot-image

PVROOT_CONTAINERS_CORE ?= "\
pv-alpine-connman \
pv-tailscale \
pv-mqtt-broker \
edge-app \
"

# Optional, installed per-customer on first boot
PVROOT_CONTAINERS ?= "\
modbus-adapter \
opcua-adapter \
"

PVROOT_IMAGE_BSP ?= "core-image-minimal"

Build, flash, and boot — the gateway comes up with all core services running. Optional containers are factory-bundled and installed on first boot per device metadata.

Failure isolation

A crashing edge-app container restarts (per its restart_policy) without affecting the MQTT broker, Tailscale, or the BSP. A failed update to the Modbus container rolls back without touching the rest of the composition. This is the practical difference between "containers" and "monolith": one bad component doesn't take down the whole gateway.

OTA per container

# Update only the protocol adapter
pvr clone https://pvr.pantahub.com/USERNAME/DEVICE_NAME ws
cd ws
pvr app update modbus --from gitlab.com/myorg/modbus-adapter:1.4.2
pvr commit -m "Modbus 1.4.2 — fix register decode"
pvr sig add --part modbus
pvr post https://pvr.pantahub.com/USERNAME/DEVICE_NAME

Other containers are untouched. Differential transfer ships only the changed object hashes — minutes over cellular instead of a full reflash.

Hardware targets

Gateway hardware that runs Pantavisor today includes Raspberry Pi 3/4/5 (see supported devices), custom ARM64/ARMv7 boards via your own MACHINE definition + meta-pantavisor, and x86 industrial PCs.

Next steps