Skip to content

marcopennelli/meta-container-deploy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

meta-container-deploy

meta-container-deploy

Yocto/OpenEmbedded layer for declarative Podman container support with build-time image pulling and systemd Quadlet integration.

Overview

meta-container-deploy provides BitBake classes and recipes to:

  1. Pull container images at build time using skopeo-native and include them in the rootfs
  2. Generate Podman Quadlet files (.container units) for declarative systemd service management
  3. Define containers declaratively via YAML/JSON manifests or BitBake variables

This enables reproducible, air-gapped container deployments for embedded Linux systems.

Features

  • Build-time container image pulling - Images are pulled during bitbake using skopeo-native
  • OCI format storage - Images stored in standard OCI layout for portability
  • Podman Quadlet integration - Modern declarative systemd container management
  • Architecture mapping - Automatic TARGET_ARCH to OCI architecture conversion
  • Private registry support - Authentication via Docker config.json
  • Dependency management - Container service ordering via systemd dependencies
  • Security options - Capabilities, security labels, read-only rootfs support

Technology Overview

Podman

Podman is a daemonless, open-source container engine for developing, managing, and running OCI containers on Linux systems. Unlike Docker, Podman doesn't require a background daemon process, making it ideal for embedded systems where resource efficiency is critical.

Key advantages for embedded Linux:

  • Daemonless architecture - No background service consuming resources
  • Rootless containers - Run containers without root privileges for enhanced security
  • Systemd integration - Native support for running containers as systemd services
  • Docker-compatible - Uses the same container images and CLI syntax as Docker

Podman Quadlet

Quadlet is Podman's native systemd integration, introduced in Podman 4.4 (February 2023). It allows you to define containers declaratively using .container files that systemd's generator converts into proper service units.

How Quadlet works:

  1. Place .container files in /etc/containers/systemd/ (or ~/.config/containers/systemd/ for user containers)
  2. On boot (or systemctl daemon-reload), Quadlet's generator parses these files
  3. Systemd creates transient service units from the container definitions
  4. Containers are managed like any other systemd service

Example .container file generated by this layer:

[Unit]
Description=nginx-server container service
After=network-online.target
Wants=network-online.target

[Container]
Image=docker.io/library/nginx:alpine
PublishPort=8080:80

[Service]
Restart=always
TimeoutStartSec=900

[Install]
WantedBy=multi-user.target

Benefits over traditional approaches:

  • Declarative - Define what you want, not how to run it
  • Integrated - Uses standard systemd dependency management
  • Reliable - Proper service lifecycle, restart policies, and logging
  • Simple - No custom scripts or wrapper services needed

Skopeo

Skopeo is a command-line utility for working with container images and registries. This layer uses skopeo-native (the host-side build tool) to pull container images during the BitBake build process.

Skopeo capabilities used by this layer:

  • Copy images between registries and local storage - Pull from Docker Hub, Quay.io, or private registries
  • Support multiple image formats - Convert between Docker v2, OCI, and other formats
  • Architecture selection - Pull images for specific CPU architectures (arm64, amd64, etc.)
  • Authentication - Support for private registry credentials via Docker config.json

Build-time vs. runtime usage:

  • Build time: skopeo-native pulls images and stores them in OCI format in the rootfs
  • Runtime: skopeo (or podman load) imports preloaded images into Podman's storage

OCI Format

The Open Container Initiative (OCI) defines industry standards for container formats and runtimes. This layer stores pulled images in OCI Image Layout format, which provides:

  • Portability - Standard format readable by any OCI-compliant tool
  • Content-addressable storage - Images identified by cryptographic digests
  • Multi-architecture support - Single image can contain manifests for multiple platforms
  • Reproducibility - Deterministic image identification via digest pinning

OCI image layout structure:

/var/lib/containers/preloaded/
└── nginx-server/
    ├── blobs/
    │   └── sha256/
    │       ├── <config-digest>
    │       ├── <layer-digests>...
    │       └── <manifest-digest>
    ├── index.json
    └── oci-layout

Architecture Mapping

Yocto's TARGET_ARCH values differ from OCI platform architectures. This layer automatically maps between them:

Yocto TARGET_ARCH OCI Architecture
aarch64 arm64
arm arm
x86_64 amd64
i686, i586 386
riscv64 riscv64
powerpc64le ppc64le

You can override automatic detection using CONTAINER_ARCH variable if needed.

Systemd Integration

This layer requires systemd as the init manager because:

  1. Quadlet depends on systemd - The generator mechanism is a systemd feature
  2. Service dependencies - Container ordering uses systemd's After=/Requires= directives
  3. Lifecycle management - Restart policies map to systemd's Restart= options
  4. Logging - Container output goes to the journal via journalctl

The boot sequence:

  1. container-import.service runs early (before container services)
  2. Import scripts load OCI images into Podman storage
  3. Quadlet generator creates service units from .container files
  4. Container services start in dependency order

Requirements

  • Yocto Project: Scarthgap (5.0) or Styhead (5.1)
  • meta-virtualization layer (for podman, skopeo recipes)
  • systemd as init manager
  • Podman 4.4+ (for Quadlet support)

Note: Kirkstone (4.0) is not supported because meta-virtualization provides Podman 4.0.x for Kirkstone, which predates Quadlet support (introduced in Podman 4.4, February 2023).

Installation

Add the layer to your bblayers.conf:

BBLAYERS += "/path/to/meta-container-deploy"

Ensure meta-virtualization is also included.

Quick Start

This layer provides 4 methods to include containers in your Yocto image:

Method Best For Configuration Location
1. Direct Recipe Static, versioned container recipes .bb recipe files
2. local.conf Variables Dynamic provisioning, CI/CD pipelines local.conf
3. Manifest-Based Standalone projects, config versioning containers.yaml
4. Packagegroup Grouping multiple container recipes Packagegroup recipe

Prerequisites

Add to your local.conf to enable systemd (required for Quadlet):

DISTRO_FEATURES:append = " systemd usrmerge virtualization"
DISTRO_FEATURES_BACKFILL_CONSIDERED:append = " sysvinit"
VIRTUAL-RUNTIME_init_manager = "systemd"
VIRTUAL-RUNTIME_initscripts = "systemd-compat-units"

# Required for container networking (iptables/netfilter modules)
IMAGE_INSTALL:append = " kernel-modules"

Method 1: Direct Recipe Approach (Traditional)

Create individual .bb recipe files that inherit the container classes.

Create a recipe:

# recipes-containers/webserver/nginx-server.bb
SUMMARY = "Nginx web server container"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

inherit container-image container-quadlet

CONTAINER_IMAGE = "docker.io/library/nginx:alpine"
CONTAINER_NAME = "nginx-server"
CONTAINER_PORTS = "8080:80"
CONTAINER_RESTART = "always"

Add to your image:

IMAGE_INSTALL:append = " nginx-server packagegroup-container-support"

Build and run:

bitbake core-image-minimal
# Boot the image, then:
systemctl status nginx-server
curl localhost:8080

Method 2: local.conf Variable-Based

Configure containers entirely in local.conf without creating recipe files. Ideal for CI/CD pipelines and dynamic provisioning systems that generate local.conf at build time.

Add to local.conf:

# List of containers to deploy
CONTAINERS = "mqtt-broker nginx-proxy"

# MQTT broker configuration
CONTAINER_mqtt_broker_IMAGE = "docker.io/eclipse-mosquitto:2.0"
CONTAINER_mqtt_broker_PORTS = "1883:1883 9001:9001"
CONTAINER_mqtt_broker_VOLUMES = "/data/mosquitto:/mosquitto/data:rw"
CONTAINER_mqtt_broker_RESTART = "always"

# Nginx proxy configuration
CONTAINER_nginx_proxy_IMAGE = "docker.io/library/nginx:alpine"
CONTAINER_nginx_proxy_PORTS = "80:80 443:443"
CONTAINER_nginx_proxy_DEPENDS_ON = "mqtt-broker"

# Add to image
IMAGE_INSTALL:append = " packagegroup-containers-localconf"

Note: Container names with - or . are converted to _ in variable names (e.g., mqtt-broker becomes CONTAINER_mqtt_broker_*).

Build and run:

bitbake core-image-minimal
# Boot the image, then:
systemctl status mqtt-broker
systemctl status nginx-proxy

Method 3: Manifest-Based

Define containers in a YAML or JSON manifest file. Ideal for standalone Yocto projects where container configuration should be version-controlled separately.

Create containers.yaml:

# containers.yaml
containers:
  - name: mqtt-broker
    image: docker.io/eclipse-mosquitto:2.0
    ports:
      - "1883:1883"
      - "9001:9001"
    volumes:
      - "/data/mosquitto:/mosquitto/data:rw"
    restart_policy: always
    enabled: true

  - name: node-red
    image: docker.io/nodered/node-red:latest
    ports:
      - "1880:1880"
    volumes:
      - "/data/node-red:/data:rw"
    depends_on:
      - mqtt-broker
    environment:
      TZ: "UTC"
    restart_policy: always

Add to local.conf:

# Path to manifest file
CONTAINER_MANIFEST = "${TOPDIR}/conf/containers.yaml"

# Add to image
IMAGE_INSTALL:append = " packagegroup-containers-manifest"

Build and run:

bitbake core-image-minimal
# Boot the image, then:
systemctl status mqtt-broker
systemctl status node-red

Method 4: Packagegroup Approach

Create a packagegroup that depends on multiple container recipes. Useful for organizing related containers into logical groups.

Create a packagegroup recipe:

# recipes-containers/packagegroups/packagegroup-iot-containers.bb
SUMMARY = "IoT container stack"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

inherit packagegroup

RDEPENDS:${PN} = "\
    packagegroup-container-support \
    mqtt-broker \
    node-red \
    grafana \
"

Add to your image:

IMAGE_INSTALL:append = " packagegroup-iot-containers"

This method requires the individual container recipes (mqtt-broker, node-red, grafana) to exist as Method 1 recipes.

BitBake Classes

container-image.bbclass

Pulls container images at build time using skopeo-native.

Variables:

Variable Description Default
CONTAINER_IMAGE Full image reference (required) -
CONTAINER_NAME Unique container name ${PN}
CONTAINER_PULL_POLICY Pull policy: always, missing, never missing
CONTAINER_AUTH_FILE Path to Docker auth config -
CONTAINER_DIGEST Pin to specific digest -
CONTAINER_ARCH Override target architecture auto-detected

container-quadlet.bbclass

Generates Podman Quadlet .container files for systemd integration.

Variables:

Variable Description Default
CONTAINER_NAME Service name ${PN}
CONTAINER_IMAGE Image reference (required) -
CONTAINER_PORTS Space-separated port mappings -
CONTAINER_VOLUMES Space-separated volume mounts -
CONTAINER_ENVIRONMENT Space-separated KEY=value pairs -
CONTAINER_NETWORK Network mode: host, bridge, none -
CONTAINER_DEPENDS_ON Space-separated service names -
CONTAINER_RESTART Restart policy always
CONTAINER_USER User to run as -
CONTAINER_WORKING_DIR Working directory -
CONTAINER_PRIVILEGED Set to "1" for privileged -
CONTAINER_READ_ONLY Set to "1" for read-only rootfs -
CONTAINER_DEVICES Space-separated device paths -
CONTAINER_CAPS_ADD Capabilities to add -
CONTAINER_CAPS_DROP Capabilities to drop -
CONTAINER_SECURITY_OPTS Security options -
CONTAINER_LABELS Space-separated key=value labels -
CONTAINER_MEMORY_LIMIT Memory limit (e.g., 512m) -
CONTAINER_CPU_LIMIT CPU limit (e.g., 0.5) -
CONTAINER_ENABLED Set to "0" to disable 1

container-localconf.bbclass

Enables container configuration via local.conf variables (Method 2). Used by packagegroup-containers-localconf.

Variables:

Variable Description
CONTAINERS Space-separated list of container names to deploy
CONTAINER_<name>_IMAGE Container image reference (required)
CONTAINER_<name>_PORTS Space-separated port mappings
CONTAINER_<name>_VOLUMES Space-separated volume mounts
CONTAINER_<name>_ENVIRONMENT Space-separated KEY=value pairs
CONTAINER_<name>_NETWORK Network mode: host, bridge, none
CONTAINER_<name>_RESTART Restart policy (default: always)
CONTAINER_<name>_DEPENDS_ON Space-separated container dependencies
CONTAINER_<name>_* All other container-quadlet variables

Note: Replace - and . in container names with _ for variable names.

container-manifest.bbclass

Parses YAML/JSON container manifests for dynamic recipe generation (Method 3). Used by packagegroup-containers-manifest.

Variables:

Variable Description
CONTAINER_MANIFEST Path to containers.yaml or containers.json

Container Manifest Format

containers:
  - name: <string>              # Required: unique container name
    image: <string>             # Required: full image reference
    entrypoint: [<string>]      # Override entrypoint
    command: [<string>]         # Command arguments
    environment:                # Environment variables
      KEY: value
    ports:                      # Port mappings
      - "host:container"
      - "host:container/protocol"
    volumes:                    # Volume mounts
      - "/host/path:/container/path[:options]"
    devices:                    # Device passthrough
      - "/dev/ttyUSB0"
    network: <string>           # host, bridge, none, or custom
    depends_on:                 # Service dependencies
      - other-container
    restart_policy: <string>    # always, on-failure, no
    user: <string>              # User to run as
    working_dir: <string>       # Working directory
    privileged: <bool>          # Privileged mode
    read_only: <bool>           # Read-only root filesystem
    memory_limit: <string>      # Memory limit (512m, 1g)
    cpu_limit: <string>         # CPU limit (0.5, 2)
    enabled: <bool>             # Enable on boot (default: true)
    capabilities_add: [<string>]
    capabilities_drop: [<string>]
    security_opts: [<string>]
    labels:
      key: value
    registry:                   # Private registry config
      url: <string>
      auth_secret: <string>     # Reference to auth file

Recipes

container-import

Systemd service that imports preloaded OCI container images into Podman storage at first boot.

packagegroup-container-support

Meta-package that pulls in all required container runtime dependencies:

  • podman
  • skopeo
  • container-import

packagegroup-containers-localconf

Entry point for Method 2 (local.conf variable-based). Add this to IMAGE_INSTALL along with CONTAINERS and CONTAINER_* variables in local.conf.

packagegroup-containers-manifest

Entry point for Method 3 (manifest-based). Add this to IMAGE_INSTALL along with CONTAINER_MANIFEST variable pointing to your containers.yaml file.

containers-localconf

Worker recipe that pulls container images and generates Quadlet files based on local.conf variables. Used internally by packagegroup-containers-localconf.

containers-manifest

Worker recipe that parses the manifest file and deploys containers. Used internally by packagegroup-containers-manifest.

test-container

Example container recipe (Method 1) deploying nginx:alpine for layer validation.

Architecture

/var/lib/containers/
├── preloaded/                  # OCI images (build-time)
│   ├── nginx-server/
│   │   ├── blobs/
│   │   ├── index.json
│   │   └── oci-layout
│   └── .imported/              # Import markers
│       └── nginx-server
└── storage/                    # Podman storage (runtime)

/etc/containers/
├── systemd/                    # Quadlet files
│   ├── nginx-server.container
│   └── mqtt-broker.container
└── import.d/                   # Per-container import scripts
    ├── nginx-server.sh
    └── mqtt-broker.sh

Boot Sequence

  1. container-import.service runs before container services
  2. Import scripts in /etc/containers/import.d/ are executed
  3. OCI images are imported into Podman storage via skopeo
  4. Import markers are created to prevent re-import
  5. Quadlet generator creates systemd units from .container files
  6. Container services start in dependency order

Contributing

Contributions are welcome! Please submit issues and pull requests on GitHub.

License

MIT License - see LICENSE file.

Author

Marco Pennelli

Related Projects

About

Yocto/OpenEmbedded layer for build-time Podman container deployment with Quadlet systemd integration

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •