How to Containerize Embedded Linux Applications

What You’ll Build

A Pantavisor container — an LXC system container with its own rootfs, init, services, and config — that ships in a state revision and runs on any Pantavisor-enabled embedded Linux device.

Pantavisor containers are system containers, not single-process app containers. Each one runs its own init, supervises its own services, and has its own filesystem tree. The runtime is LXC, not Docker.

Three Sources

pvr app add accepts three input types. Pick the one that matches what you already have:

Source When to use Command
Docker / OCI image You already publish to Docker Hub / GHCR / GitLab Registry pvr app add NAME --from=docker://image:tag
Yocto-built rootfs You build with Yocto and want full control pvr app add NAME --from=path/to/rootfs -t rootfs
GitLab PVR package The container is already published as a .pvrexport.tgz pvr app add NAME -t pvr --from=https://gitlab.com/.../package

Prerequisites

  • A PVR workspace (cloned device state or fresh pvr init):

    pvr clone https://pvr.pantahub.com/<USER>/<DEVICE_NICK> my-device
    cd my-device

Method 1 — From a Docker / OCI Image

This is the fastest path if your application already builds as a Docker image.

# Add an upstream image
pvr app add nginx --from=docker://nginx:alpine

# Pin the architecture for cross-arch builds
pvr app add nginx --platform=linux/arm64 --from=docker://nginx:alpine

# Custom args.json / config.json
pvr app add myapp --from=docker://myorg/myapp:1.2.3 \
  --args-json=args.json \
  --config-json=config.json

PVR pulls the image, flattens its layers into a Pantavisor LXC container, generates run.json, and stages everything for commit. Your Dockerfile, registry, and CI keep working — Docker becomes the input format, not the runtime.

Method 2 — From a Yocto-Built Rootfs

When you need fine-grained control over what’s inside the container — typically for production or certified devices — build the rootfs with Yocto and inherit container-pvrexport:

SUMMARY = "My embedded application container"
LICENSE = "MIT"

inherit core-image container-pvrexport

IMAGE_BASENAME = "myapp"
IMAGE_FSTYPES = "pvrexportit"

# What goes inside the container
IMAGE_INSTALL += "busybox myapp-binaries"

# Container runtime configuration
SRC_URI += "file://args.json file://config.json"

# Group placement
PVR_APP_ADD_GROUP = "platform"

# Extra LXC volume mounts
PVR_APP_ADD_EXTRA_ARGS += " \
    --volume ovl:/tmp:permanent \
"

The container-pvrexport class produces a .pvrexport.tgz artifact that gets deployed by pvroot-image into /trails/0/ of the flashable image, or added to a running device with pvr app add -t pvr.

Method 3 — From a Local Rootfs Directory

Useful when you have a rootfs tarball or directory from any source:

# From a directory
pvr app add myapp -t rootfs --from=~/Desktop/myapp-rootfs

# From a tarball (local or URL)
pvr app add myapp -t rootfs --from=https://example.com/rootfs.tar

Configure the Container

Every container can carry runtime metadata:

// args.json — passed to the container at start
{
  "PV_GROUP": "platform",
  "PV_DRIVERS_OPTIONAL": ["wifi", "usbnet"]
}
// run.json — generated by pvr; tweak only when needed
{
  "group": "platform",
  "name": "myapp",
  "restart_policy": "system",
  "root-volume": "root.squashfs",
  "type": "lxc"
}

Overlay configuration files at _config/<container>/... are mounted on top of the container’s root at runtime — perfect for environment-specific settings without rebuilding the image.

Commit and Deploy

pvr add .
pvr commit -m "Add myapp container"
pvr sig add --parts=myapp
pvr post https://pvr.pantahub.com/<USER>/<DEVICE_NICK>

The device pulls the new revision, mounts the new container, and starts it. If the container fails its status_goal, the device rolls back automatically.

Inter-Container Communication

Containers can export and consume services through the pv-xconnect service mesh. Declare exported sockets in services.json:

{
  "#spec": "service-manifest-xconnect@1",
  "services": [
    {"name": "metrics", "type": "unix", "socket": "/run/metrics.sock"}
  ]
}

Consumers declare dependencies in args.json via PV_SERVICES_REQUIRED.

Common Pitfalls

  • Wrong architecture--platform defaults to host; for cross-arch devices set explicitly (linux/arm, linux/arm64, etc.) or configure _hostconfig/pvr/docker.json.
  • Single-process Docker images — Pantavisor containers are system containers; an image expecting CMD ["app"] and nothing else may need a real init or service supervisor inside.
  • Skipping args.json — defaults work but environment variables, group placement, and required services live there. Set them explicitly for production containers.

Next Steps