Yocto/OpenEmbedded layer for declarative Podman container support with build-time image pulling and systemd Quadlet integration.
meta-container-deploy provides BitBake classes and recipes to:
- Pull container images at build time using skopeo-native and include them in the rootfs
- Generate Podman Quadlet files (.container units) for declarative systemd service management
- Define containers declaratively via YAML/JSON manifests or BitBake variables
This enables reproducible, air-gapped container deployments for embedded Linux systems.
- 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
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
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:
- Place
.containerfiles in/etc/containers/systemd/(or~/.config/containers/systemd/for user containers) - On boot (or
systemctl daemon-reload), Quadlet's generator parses these files - Systemd creates transient service units from the container definitions
- 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.targetBenefits 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 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-nativepulls images and stores them in OCI format in the rootfs - Runtime:
skopeo(orpodman load) imports preloaded images into Podman's storage
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
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.
This layer requires systemd as the init manager because:
- Quadlet depends on systemd - The generator mechanism is a systemd feature
- Service dependencies - Container ordering uses systemd's
After=/Requires=directives - Lifecycle management - Restart policies map to systemd's
Restart=options - Logging - Container output goes to the journal via
journalctl
The boot sequence:
container-import.serviceruns early (before container services)- Import scripts load OCI images into Podman storage
- Quadlet generator creates service units from
.containerfiles - Container services start in dependency order
- 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).
Add the layer to your bblayers.conf:
BBLAYERS += "/path/to/meta-container-deploy"Ensure meta-virtualization is also included.
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 |
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"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:8080Configure 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-proxyDefine 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: alwaysAdd 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-redCreate 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.
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 |
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 |
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.
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 |
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 fileSystemd service that imports preloaded OCI container images into Podman storage at first boot.
Meta-package that pulls in all required container runtime dependencies:
- podman
- skopeo
- container-import
Entry point for Method 2 (local.conf variable-based). Add this to IMAGE_INSTALL along with CONTAINERS and CONTAINER_* variables in local.conf.
Entry point for Method 3 (manifest-based). Add this to IMAGE_INSTALL along with CONTAINER_MANIFEST variable pointing to your containers.yaml file.
Worker recipe that pulls container images and generates Quadlet files based on local.conf variables. Used internally by packagegroup-containers-localconf.
Worker recipe that parses the manifest file and deploys containers. Used internally by packagegroup-containers-manifest.
Example container recipe (Method 1) deploying nginx:alpine for layer validation.
/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
container-import.serviceruns before container services- Import scripts in
/etc/containers/import.d/are executed - OCI images are imported into Podman storage via skopeo
- Import markers are created to prevent re-import
- Quadlet generator creates systemd units from
.containerfiles - Container services start in dependency order
Contributions are welcome! Please submit issues and pull requests on GitHub.
MIT License - see LICENSE file.
Marco Pennelli
- Podman - Daemonless container engine
- Podman Quadlet - Systemd integration
- Skopeo - Container image operations
- meta-virtualization - Yocto virtualization layer
