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:

  • Stable BSP for the chosen hardware (RPi, NXP, TI, etc.)
  • Networking (Ethernet, WiFi, LTE, sometimes LoRa)
  • Secure remote access (VPN, mesh networking)
  • Protocol adapters (Modbus, OPC-UA, CAN, BACnet, custom serial)
  • Local broker / buffer (MQTT, Kafka client, 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 (RPi 4 ARMv8)
├── platform/         # 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 args.json, config.json, and overlay configs in _config/<container>/.
  • Declares dependencies on other containers via PV_SERVICES_REQUIRED.
  • Has its own status_goal so failures are caught and rolled back automatically.

Why Composability Matters Here

Different teams, different cadences

The kernel team may release 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 “WiFi-only gateway” and a “WiFi + LTE gateway” can share BSP, platform, 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. Pantahub channels deliver the right composition to the right devices.

Building One — Concrete Example

Using the Yocto path (see Build an Embedded Linux Image from Containers):

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

inherit image pvroot-image

PVROOT_CONTAINERS_CORE ?= "\
    pv-alpine-connman   \\  # networking
    pv-tailscale        \\  # mesh VPN
    pv-mqtt-broker      \\  # local MQTT
    edge-app            \\  # custom processing
"

PVROOT_CONTAINERS ?= "\
    modbus-adapter      \\  # optional, install per-customer
    opcua-adapter       \\
"

PVROOT_IMAGE_BSP ?= "core-image-minimal"

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

Adding Tailscale Mesh VPN

A common gateway requirement is zero-config remote access. Wrap an existing Tailscale Docker image as a Pantavisor container:

pvr app add tailscale --from=docker://tailscale/tailscale:latest \
  --args-json=tailscale-args.json

The container runs alongside everything else, exposes the Tailscale interface to other containers via shared network namespace or host networking, and updates independently of the rest of the stack.

Per-Container Resource Limits

Each container can declare CPU, memory, and I/O limits in its run.json / LXC config:

  • Edge-app gets the lion’s share of CPU.
  • Modbus poller gets a small slice with high priority.
  • Logging container is capped so a runaway log doesn’t OOM the gateway.

Failure Isolation

A crashing edge-app container restarts (per its restart_policy) without affecting 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/<USER>/<DEVICE_NICK> ws
cd ws
pvr app update modbus --from=https://gitlab.com/myorg/modbus-adapter:1.4.2
pvr commit -m "Modbus 1.4.2 — fix register decode"
pvr sig add --parts=modbus
pvr post https://pvr.pantahub.com/<USER>/<DEVICE_NICK>

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

Fleet-Wide Rollouts

Use Pantahub channels (see Manage an Embedded Linux Device Fleet) to stage rollouts: deploy the new Modbus container to 50 gateways, monitor error rates, then propagate to the rest of the fleet automatically.

Hardware Targets

Common gateway hardware that runs Pantavisor today:

  • Raspberry Pi 3/4/5 — supported by meta-pantavisor, pre-built images at docs.pantahub.com/initial-devices
  • Custom ARM64 / ARMv7 boards — via your own MACHINE definition + meta-pantavisor
  • x86 industrial PCs — via meta-pantavisor with x86 BSP

See Raspberry Pi Embedded Linux with Containers for the RPi-specific quick start.

Next Steps