Image Template Reference#
Templates are YAML files that define what goes into a custom OS image, the target platform, packages, disk layout, users, and build-time customizations. This document is the authoritative field-by-field reference for the template format.
For a conceptual overview of how templates fit into the build pipeline, see Understanding the Build Process.
Table of Contents#
What Are Templates and How Do They Work?#
Templates are predefined build specifications that serve as a foundation for building operating system images. Here’s what templates empower you to do:
Create standardized baseline configurations.
Impose consistency across multiple images.
Reduce duplication of effort.
Share and reuse common configurations with your team.
The ICT provides default image templates on a per-distribution basis and image type (RAW vs. ISO) that can be used directly to build an operating system from those defaults. You can override these default templates by providing your own template and configure or override the settings and values you want. The tool will internally merge the two to create the final template used for image composition.
How Templates Work#
ICT ships default templates for each distribution and image type (raw, ISO, initrd). When you provide a user template, the tool merges it with the matching default; your values override or extend the defaults. The merged result is validated against a JSON schema before the build begins.
Default templates live at:
config/osv/<target.os>/<target.dist>/imageconfigs/defaultconfigs/default-<imageType>-<arch>.yml
Note:
imageType: imgmaps todefault-initrd-<arch>.yml(there is nodefault-img-filename).
You do not need to edit the defaults. You can start from one of the examples in
image-templates/ and override only what you need.
Quick Start Example#
A minimal user template only needs image, target, and optionally
systemConfig with extra packages:
image:
name: my-edge-device
version: "1.0.0"
target:
os: edge-microvisor-toolkit
dist: emt3
arch: x86_64
imageType: raw
systemConfig:
name: edge
packages:
- cloud-init
- rsyslog
Everything else (disk layout, bootloader, kernel, default packages) comes from
the default template for emt3 / raw / x86_64.
Top-Level Structure#
A template file has up to five top-level sections plus an optional metadata
block:
metadata: # Optional - AI-searchable discovery metadata
...
image: # Required - image name and version
...
target: # Required - OS, distribution, architecture, image type
...
baseline: # Optional - "create" (default) or "overlay" an existing image
...
overlayPolicy: # Optional - overlay-mode policy (only with baseline.mode: overlay)
...
disk: # Optional - disk layout, partitions, output artifacts
...
packageRepositories: # Optional - additional package repositories
- ...
systemConfig: # Required in merged template - packages, kernel, users, etc.
...
Note: User templates require only
imageandtarget. The remaining sections are merged from the default template if omitted.
Field Reference#
metadata#
Optional block for AI-powered template discovery. Ignored by the build engine.
Field |
Type |
Description |
|---|---|---|
|
string |
Human-readable description of the template |
|
string[] |
Use cases this template targets |
|
string[] |
Keywords for search and discovery |
metadata:
description: "Edge device image with container runtime"
use_cases: ["edge computing", "IoT gateway"]
keywords: [edge, docker, emt3]
image (required)#
Image identification. Both fields are required.
Field |
Type |
Required |
Validation |
Description |
|---|---|---|---|---|
|
string |
Yes |
|
Image name (alphanumeric, hyphens, underscores) |
|
string |
Yes |
Semver-like: |
Version string |
image:
name: my-edge-device
version: "1.0.0"
target (required)#
Target platform. All four fields are required.
Field |
Type |
Required |
Valid Values |
Description |
|---|---|---|---|---|
|
string |
Yes |
|
Target operating system |
|
string |
Yes |
See OS constraints below |
Distribution identifier |
|
string |
Yes |
|
Target CPU architecture |
|
string |
Yes |
|
Output image format |
OS → dist constraints:
OS |
Valid |
|---|---|
|
|
|
|
|
|
|
|
|
Any (e.g., |
target:
os: ubuntu
dist: ubuntu24
arch: x86_64
imageType: raw
WSL-Compatible Images#
Set target.imageType: wsl2 to compose a WSL-compatible root filesystem.
target:
os: ubuntu
dist: ubuntu24
arch: x86_64
imageType: wsl2
For a complete Ubuntu 24 WSL example, see
image-templates/ubuntu24/ubuntu24-x86_64-agentic-wsl2.yml.
baseline#
Selects how the image is assembled. If omitted, the build defaults to create
mode (build the image from scratch, the behavior described everywhere else in
this reference). Set mode: overlay to instead layer packages onto an existing
baseline disk image without rebuilding it. The baseline may be a RAW image or a
qcow2/vhd/vhdx image, which is converted to RAW before the overlay runs.
Field |
Type |
Required |
Valid Values |
Description |
|---|---|---|---|---|
|
string |
No |
|
Assembly mode |
|
object |
Yes when |
— |
The baseline image to overlay |
baseline.source#
Identifies the baseline image. Exactly one of path or url must be set.
The source is copied into the build workspace first and is never modified in
place.
Field |
Type |
Required |
Description |
|---|---|---|---|
|
string |
one of |
Local filesystem path to the baseline image (no URI scheme) |
|
string |
one of |
|
|
string |
No |
Baseline image format: |
|
string |
No |
Local filesystem path (no URI scheme) to an externally-supplied SPDX SBOM (JSON) describing the baseline image. Defaults to unset. See SBOM generation below |
baseline:
mode: overlay
source:
path: /path/to/ubuntu-24.04-base.img
format: raw
# Optional: combine this base-image SBOM with the overlay delta to emit a
# full SBOM. Omit it to use the SBOM embedded in the baseline image, if any.
sbomPath: /path/to/ubuntu-24.04-base.spdx.json
SBOM generation (overlay mode)#
An overlay build emits SPDX SBOM sidecar files into the build output directory,
alongside the emitted image (<image-name>-<version>.raw):
Artifact |
Name |
Contents |
|---|---|---|
Delta SBOM |
|
Always emitted. Only the overlay-induced package changes — the packages the overlay added and, in |
Complete SBOM |
|
Emitted only when a base SBOM is available (see below). The full final image inventory — the baseline packages plus the overlay result. When no base SBOM exists the complete document would be identical to the delta, so this sidecar is skipped. |
Both are SPDX 2.3 JSON, the same format the standalone SBOM/inspect tooling uses.
The complete SBOM is also embedded inside the image at /usr/share/sbom
(replacing the SBOM the image inherited from the baseline, so the in-image
manifest reflects the full final inventory).
The complete SBOM is the union of a base SBOM and the overlay delta. Which base is used follows this order:
baseline.source.sbomPathset and valid — that externally-supplied base SBOM is combined with the delta.sbomPathunset — the SBOM the baseline image itself carries at/usr/share/sbomis used as the base and combined with the delta.No base SBOM available —
sbomPathis unset or the file is absent/unreadable/malformed and the baseline image embeds no SBOM — then there is no full inventory to build, so the complete sidecar is skipped (it would be identical to the delta) while the in-image/usr/share/sbommanifest and the delta sidecar are still written. This is never an error: the build succeeds.
A missing or malformed sbomPath falls back to the baseline-embedded SBOM (then
to delta-only); it does not fail the build. Omitting sbomPath entirely
preserves the pre-existing base-resolution behavior. This applies to every
overlay-capable provider (Ubuntu and Debian).
To diff these artifacts against the baseline — the overlay RAW vs the baseline RAW, or the complete SBOM vs the baseline SBOM — see Comparing Overlay Outputs.
Note: Overlay mode is currently wired end-to-end for the Ubuntu and Debian providers. Targeting a provider without overlay support fails the build immediately with a clear message rather than silently falling back to a create-mode build. The overlay build is additive by default: packages (and their transitive dependencies not already present in the baseline) are installed into the baseline root, the initramfs is regenerated for the added packages, the template’s
systemConfig.configurationscommands andsystemConfig.additionalFilesare applied, and an optional grow-only resize can enlarge the image to a largerdisk.size. Existing baseline packages are not removed unlessoverlayPolicy.allowPackageRemovalis enabled, which permits a conflict-driven removal of a baseline package a to-install package conflicts with. The installed bootloader binary, the ESP, and the bootable kernel are never modified regardless of policy.OS defaults do not apply. Unlike a create-mode build, an overlay template is not merged with the target’s create-mode OS default configuration (
default-raw-*.yml). Those defaults describe how to build an image from scratch — disk size and partition table, bootloader, kernel, and the base OS package set — all of which the baseline image already provides. The overlay pipeline reads disk/bootloader/kernel geometry from the detected baseline, not from the template, so the effective overlay template is exactly what you declare (folded through anyextends:chain) with nothing inherited from the OS default. In particular, an overlay template that omitsdisk.sizekeeps the baseline’s size — it does not pick up the default’sdisk.size, which against a larger baseline would otherwise be rejected as a shrink.Unsupported systemConfig sections. Because overlay mode does not re-run the boot/system-provisioning stages, the following
systemConfigsections cannot be applied to an overlay build:hostname,network,initramfs,kernel,immutability,fde, andbootloader. Previously these were silently ignored; now setting any of them in an overlay template fails the build up front with a message naming every offending section. Configure them in the baseline image (acreate-mode build) instead. The overlay-supportedsystemConfiginputs arepackages,users,configurations, andadditionalFiles.Users in overlay.
systemConfig.usersis provisioned onto the baseline. A requested user that already exists in the baseline image fails the build up front (an overlay cannot redefine a baseline account); the check is re-run immediately before creation, so a name that a package’s maintainer script adds during install also fails rather than being silently modified. A user’sstartupScriptmust reference a path already present when users are created — i.e. shipped by the baseline or installed by an overlaypackagesentry — not a file delivered viaadditionalFiles, which are copied later in the overlay pipeline.The following
systemConfig.usersfields are not currently applied (an inherited create-mode limitation, in both create and overlay builds):home,shell, andpasswordMaxAge. The login shell is always set to/bin/bash, so a service account cannot yet be pinned to/usr/sbin/nologinvia the template.Sizing: Adding packages does not auto-grow the image, and the overlay preserves the baseline disk layout by default. Growing the image is opt-in: it requires both a
disk.sizelarger than the baseline andoverlayPolicy.allowDiskResize: true. Whendisk.sizeis larger butallowDiskResizeis not set, the build fails early with a clear message rather than silently resizing the baseline. The resize is grow-only and keyed solely ondisk.size(compared to the baseline’s current size), not on how much space the added packages need. If the baseline root is near-full, setdisk.sizelarger than the baseline image and enableallowDiskResizeto make room; otherwise the package install step fails with a “no space left on device” error (the failure message points back here).Resize constraints. The grow-only resize extends the last partition on the disk and its filesystem in place. It is rejected (before any disk mutation, with an actionable error) when the root is not the last partition, when the root sits on LVM, and when the root is LUKS-encrypted or dm-verity protected.
ext4/ext3/ext2roots are the supported and CI-covered target;xfsroots use the same code path (xfs_growfs) but are best-effort: the grow sequence has unit-test coverage, but no shipping baseline exercises it against a real xfs filesystem in CI/e2e, so treat xfs resize as unverified end-to-end. The resize shells out togrowpart(cloud-guest-utils),sgdisk(gdisk, GPT only),resize2fs(e2fsprogs) orxfs_growfs(xfsprogs), andlosetup/partx(util-linux); these must be present on the build host, and the build fails early with a clear message if any is missing. Resize also reads partition start sectors vialsblk -o PATH,START,TYPE, which requires util-linux >= 2.38; Ubuntu 22.04’s stocklsblk/resize2fsare too old and not upgradable viaapt— see the util-linux/e2fsprogs build-from-source instructions.
overlayPolicy#
Optional policy controls for overlay-mode preflight and install. It is a
top-level peer of baseline and may only be set when baseline.mode is
overlay. If omitted, the defaults below apply.
Field |
Type |
Required |
Valid Values |
Description |
|---|---|---|---|---|
|
string |
No |
|
Permitted package operations. |
|
string |
No |
|
How a package conflict detected during preflight is handled. |
|
string |
No |
— |
Optional kernel command-line override applied to the overlaid image |
|
boolean |
No |
|
Permit growing the baseline image to satisfy a larger |
|
boolean |
No |
|
Permit removing a baseline package that a to-install package conflicts with (e.g. installing |
additive-and-upgradescope. Upgrades apply only to the package set: a package already installed in the baseline may be replaced by a newer version when the resolved overlay closure requires it. Downgrades are still rejected at preflight, and the baseline kernel and bootloader remain immutable — an overlay never replaces the kernel or reinstalls the bootloader, regardless ofpackageOperation. Chooseadditive-only(the default) to fail the build on any version bump to a baseline package.
Package removal (
allowPackageRemoval). Opt-in, and permitted only underpackageOperation: additive-and-upgrade— removal is more invasive than an in-place upgrade, so pairing it with the defaultadditive-onlyis rejected at validation. By default an overlay never removes a baseline package: a to-install package thatConflicts:/Breaks:a present baseline package fails the build. SetallowPackageRemoval: true(withadditive-and-upgrade) to let the overlay remove the conflicting baseline package before installing its replacement — the case that makesdracut(which conflicts withinitramfs-tools) installable on a stock baseline. Bootloader and bootable-kernel packages are still never removed, so the flag cannot break the boot path.
baseline:
mode: overlay
source:
path: /path/to/ubuntu-24.04-base.img
overlayPolicy:
# Removal requires additive-and-upgrade (not the default additive-only).
packageOperation: additive-and-upgrade
conflictPolicy: fail
# Opt in to removing a baseline package a new package conflicts with
# (e.g. remove initramfs-tools so dracut can install). Default: false.
allowPackageRemoval: true
A complete additive-only starter template (the default policy, without package
removal) lives at
image-templates/ubuntu24/ubuntu24-x86_64-overlay-raw.yml;
to enable removal, add the overlayPolicy block shown above (additive-and-upgrade
plus allowPackageRemoval: true) to it.
disk#
Disk layout, partition scheme, and output artifact formats. If omitted, the default template provides sensible values (typically 4–6 GiB GPT disk with EFI boot and ext4 root partitions).
Field |
Type |
Required |
Description |
|---|---|---|---|
|
string |
Yes (schema) |
Disk configuration name (e.g., |
|
string |
No |
Disk device path (used by live installer, e.g., |
|
string |
No |
Disk size. Accepts: |
|
string |
No |
|
|
artifact[] |
No |
Output formats and optional compression |
|
partition[] |
No |
Partition layout definitions |
disk.artifacts[]#
Each entry defines one output format:
Field |
Type |
Required |
Valid Values |
Description |
|---|---|---|---|---|
|
string |
Yes |
|
Output image format |
|
string |
No |
|
Compression to apply |
disk.partitions[]#
Each entry defines one partition:
Field |
Type |
Description |
|---|---|---|
|
string |
Partition identifier (e.g., |
|
string |
Partition label |
|
string |
Partition type (e.g., |
|
string |
GPT type GUID (e.g., |
|
string |
Filesystem type: |
|
string |
Filesystem label |
|
string |
Start offset (e.g., |
|
string |
End offset ( |
|
string |
Mount point (e.g., |
|
string |
Mount options (e.g., |
|
string[] |
Partition flags (e.g., |
Example - raw disk with two partitions and two output formats:
disk:
name: Edge_Raw
size: 4GiB
partitionTableType: gpt
artifacts:
- type: raw
compression: gz
- type: vhdx
partitions:
- id: boot
type: esp
flags: [esp, boot]
start: 1MiB
end: 513MiB
fsType: fat32
mountPoint: /boot/efi
mountOptions: umask=0077
- id: rootfs
type: linux-root-amd64
start: 513MiB
end: "0"
fsType: ext4
mountPoint: /
mountOptions: defaults
packageRepositories#
Optional list of additional package repositories beyond the OS base repos.
Field |
Type |
Required |
Description |
|---|---|---|---|
|
string |
Yes |
Repository identifier (e.g., |
|
string |
Yes |
Repository base URL (must be a valid URI) |
|
string |
Yes |
GPG key URL, absolute file path, or |
|
string |
No |
Repository component (e.g., |
|
int |
No |
Priority from |
|
string[] |
No |
Specific packages to include from this repo (package pinning) |
packageRepositories:
- codename: "company-internal"
url: "https://packages.example.com/repo"
pkey: "https://packages.example.com/gpg.key"
component: "main"
priority: 100
- codename: "dev-tools"
url: "https://dev.example.com/repo"
pkey: "[trusted=yes]"
See Multiple Package Repository Support for detailed configuration guidance.
systemConfig#
System configuration - packages, kernel, users, bootloader, build-time commands, and more. Required in the final merged template, but optional in user templates (as defaults already provide a complete base).
Field |
Type |
Required |
Description |
|---|---|---|---|
|
string |
No |
Configuration name |
|
string |
No |
Human-readable description |
|
string |
No |
System hostname |
|
string[] |
No |
Packages to install (additive with defaults) |
|
object |
No |
Kernel configuration |
|
object |
No |
Bootloader configuration |
|
object |
No |
dm-verity / Secure Boot configuration |
|
user[] |
No |
User account definitions |
|
object |
No |
Initramfs config (ISO/initrd builds) |
|
file[] |
No |
Extra files to copy into the image |
|
cmd[] |
No |
Shell commands to run during build |
Package names must match: ^[A-Za-z0-9](?:[A-Za-z0-9+_.:~-]*[A-Za-z0-9+])?$
and must be unique within the list.
systemConfig.kernel#
Field |
Type |
Description |
|---|---|---|
|
string |
Kernel version (e.g., |
|
string |
Kernel boot command line |
|
string[] |
Kernel packages (e.g., |
|
string |
Additional kernel modules to load |
|
bool |
Enable Unified Kernel Image (typically set by defaults) |
systemConfig:
kernel:
version: "6.14"
cmdline: "console=ttyS0,115200 console=tty0 loglevel=7"
packages:
- linux-image-generic-hwe-24.04
# Optional additional repositories
packageRepositories:
- codename: emtNext
url: https://example.com/rpms/next/base
pkey: https://example.com/RPM-GPG-KEY
priority: 1001
allowPackages:
- kernel-6.17.11
- kernel-drivers-gpu-6.17.11
- libva*
- codename: edgeai
url: https://example2.com/edgeai/
pkey: https://example2.com/edgeai/GPG-PUB-KEY.gpg
priority: 500
systemConfig.bootloader#
Field |
Type |
Valid Values |
Description |
|---|---|---|---|
|
string |
|
Boot firmware type |
|
string |
|
Bootloader software |
Typical defaults: raw images use efi / systemd-boot; ISO images use
efi / grub.
systemConfig.network#
Declarative network configuration for the installed OS. This is a minimal explicit-interface implementation.
Field |
Type |
Required |
Valid Values |
Description |
|---|---|---|---|---|
|
string |
Yes (when section present) |
|
Network configuration backend |
|
object[] |
No |
See below |
List of interface configurations |
interfaces[] fields:
Field |
Type |
Required |
Description |
|---|---|---|---|
|
string |
Yes |
Interface name (for example, |
|
bool |
No |
Enable DHCPv4 |
|
bool |
No |
Enable DHCPv6 |
|
string[] |
No |
Static addresses in CIDR format |
|
object[] |
No |
Static routes (see below) |
|
string[] |
No |
DNS server addresses |
routes[] fields:
Field |
Type |
Required |
Description |
|---|---|---|---|
|
string |
Yes |
Destination ( |
|
string |
Yes |
Gateway address |
Note: Interface names are explicit and user-provided. The current implementation does not auto-discover or auto-select NICs at install time.
When
backendissystemd-networkd, the builder enables thesystemd-networkdservice in the image. Whenbackendisnetplan,systemd-networkdis not forcibly enabled — netplan manages its own renderer.
systemConfig:
network:
backend: systemd-networkd
interfaces:
- name: enp1s0
dhcp4: true
- name: enp2s0
addresses:
- "10.0.0.100/24"
routes:
- to: default
via: "10.0.0.1"
nameservers:
- "8.8.8.8"
- "8.8.4.4"
systemConfig:
network:
backend: netplan
interfaces:
- name: enp1s0
dhcp4: true
- name: enp2s0
addresses:
- "192.168.1.10/24"
routes:
- to: default
via: "192.168.1.1"
nameservers:
- "1.1.1.1"
systemConfig.immutability#
Configures dm-verity immutable root filesystem and optional UEFI Secure Boot signing.
Field |
Type |
Required |
Description |
|---|---|---|---|
|
bool |
Yes (when section present) |
Enable dm-verity immutable root |
|
string |
Conditional |
Private key file ( |
|
string |
Conditional |
Certificate in PEM format ( |
|
string |
Conditional |
Certificate in DER format ( |
Note: If any Secure Boot field is provided, all three must be provided and
enabledmust betrue.
systemConfig:
immutability:
enabled: true
secureBootDBKey: /path/to/db.key
secureBootDBCrt: /path/to/db.crt
secureBootDBCer: /path/to/db.cer
systemConfig.fde#
Configures LUKS2 full-disk encryption for selected partitions.
Field |
Type |
Required |
Description |
|---|---|---|---|
|
bool |
Yes |
Enable full-disk encryption |
|
string |
Yes (when |
Absolute or template-relative local file path containing passphrase material |
|
string[] |
No |
Disk partition IDs to encrypt (defaults to the root partition) |
|
string |
No |
Boot unlock mode: |
systemConfig:
fde:
enabled: true
passphraseFile: "/run/secrets/fde-passphrase.txt"
unlock: auto
partitions:
- rootfs
See Configure Full-Disk Encryption for a complete guide, including usage with dm-verity immutability.
systemConfig.users[]#
Field |
Type |
Required |
Description |
|---|---|---|---|
|
string |
Yes |
Username |
|
string |
No |
Password (plain text or pre-hashed with |
|
string |
No |
Hash algorithm: |
|
int |
No |
Max password age in days |
|
string |
No |
Script to run on login |
|
string[] |
No |
Additional groups |
|
bool |
No |
Grant sudo permissions |
|
string |
No |
Custom home directory |
|
string |
No |
Login shell (e.g., |
systemConfig:
users:
- name: admin
password: "changeme"
sudo: true
groups: [docker, wheel]
shell: /bin/bash
- name: service-account
shell: /usr/sbin/nologin
systemConfig.initramfs#
Used for ISO and initrd builds. Points to the initramfs configuration template.
Field |
Type |
Required |
Description |
|---|---|---|---|
|
string |
Yes (when section present) |
Path to the initramfs config template file |
systemConfig.additionalFiles[]#
Copy host files into the image at build time.
Field |
Type |
Description |
|---|---|---|
|
string |
Source path on the host (absolute, or relative to template directory) |
|
string |
Destination path inside the image |
|
string |
Overlay-only. When to copy the file relative to initramfs/boot regeneration: |
systemConfig:
additionalFiles:
- local: files/dhcp.network
final: /etc/systemd/network/dhcp.network
- local: files/motd
final: /etc/motd
# A dracut module must be in place BEFORE the initramfs is (re)built, or the
# already-built initramfs would ignore it — mark it pre-initramfs.
- local: files/90-custom.conf
final: /usr/lib/dracut/dracut.conf.d/90-custom.conf
stage: pre-initramfs
Overlay mode:
additionalFilesare honored in overlay builds, and each entry’s copy timing is controlled by itsstagemarker:
stage: ""(default) — copied as the last build step, after both initramfs and GRUB regeneration. This is the historical behavior, so existing templates are unaffected. It is deliberate: a prebuilt boot artifact (for example a custom/boot/initrd.img-*) dropped here survives, whereas a file placed before regeneration would be overwritten byupdate-initramfs.
stage: pre-initramfs— copied before boot/initramfs regeneration, so content the generator consumes (a dracut module under/usr/lib/dracut, an initramfs-tools hook under/etc/initramfs-tools/) is in place when the initramfs is rebuilt. Without this, such a file dropped at the end would be ignored by the already-built initramfs.A file only needs
pre-initramfswhen the initramfs build must see it; a file that must instead run a generator itself still belongs in asystemConfig.configurationscommand.
systemConfig.configurations[]#
Shell commands executed inside the chroot during the configuration stage.
Field |
Type |
Description |
|---|---|---|
|
string |
Shell command to execute |
systemConfig:
configurations:
- cmd: systemctl enable docker
- cmd: echo "BuildDate=$(date)" >> /etc/image-info
Package Repositories#
Use packageRepositories to add extra Debian or RPM repositories to a build.
Each entry defines where to fetch package metadata and how candidates are
selected when the same package exists in multiple repositories.
Repository Fields#
codename: repository identifier.url: repository base URL.component: optional Debian component (for example,main,universe) for multi-component repositories.pkey: GPG key reference; supportshttp:///https://URLs,file://URLs, absolute local paths, or[trusted=yes]for supported Debian flows.priority: numeric repository preference used in conflict resolution.allowPackages: optional package white list for metadata filtering.
Priority Behavior#
priority is evaluated during package candidate selection across repositories.
Higher numeric values are preferred.
Debian resolver also supports APT-like behavior:
< 0: block packages from that repository990: prefer over default repositories1000: install even if lower version> 1000: force preference
When candidates have equivalent priority, version constraints and dependency context determine the final package choice.
AllowPackages White List#
allowPackages limits which package names are indexed from a specific
repository.
If omitted or empty, all repository packages are eligible.
If present, only matching package names are indexed.
Supported matching modes:
exact name (for example
spice-server)prefix/version pin (for example
kernel-6.17.11)glob patterns (for example
libva*,wayland*)
Filtering happens at metadata-parse time, before dependency resolution.
Template Merge Behavior#
When your user template is merged with the default template, different sections follow different strategies:
Section |
Strategy |
|---|---|
|
User overrides default if non-empty |
|
User value used entirely |
|
User replaces entire default if non-empty |
|
Additive - user packages appended to defaults (deduplicated) |
|
User overrides |
|
User overrides individual fields if non-empty |
|
User overrides |
|
Merged by |
|
Merged by |
|
Additive - user commands appended after defaults |
|
Merged only if user explicitly provides the section |
|
Merged only if user explicitly provides the section |
|
Merged by |
Template Extends (Inheritance)#
User templates can inherit from another template with the optional
extends: field. This lets you keep minimal delta templates that pick up
updates from their parent automatically, while still overriding anything
you need to change. Each template inherits from at most one parent
(single inheritance), so a chain is always a simple linear sequence
root → ... → leaf — no diamond problem.
Syntax#
extends: is a top-level string field. Its value is a path to a parent
template file, resolved relative to the child template’s directory.
.yml and .yaml are both accepted; symbolic links are not.
The canonical two-level example ships in the repo at
image-templates/ubuntu24/ubuntu24-x86_64-extends-example-raw.yml:
# Inherit everything (disk layout, systemConfig, immutability, ...) from the
# parent template, resolved relative to this file's directory. The parent's
# target must match this template's target. Values below override or extend the
# parent using the standard merge strategies (packages are additive).
extends: "ubuntu24-x86_64-minimal-raw.yml"
image:
name: ubuntu24-x86_64-extends-example
version: "24.04"
target:
os: ubuntu # Must match the parent template's target
dist: ubuntu24
arch: x86_64
imageType: raw
systemConfig:
name: Extends_Example
# These packages are merged additively on top of the parent's package list.
packages:
- htop
- curl
Merge Behavior in a Chain#
The same per-section rules that govern the user↔default merge
above apply at every level of an extends
chain. Because MergeConfigurations(child, parent) is a pure function of
two inputs, applying it iteratively as a fold produces the same set of
merged fields at any depth: if the merge is deterministic for two
layers, it is deterministic for N. Note that the ordering of slice
entries merged by key (systemConfig.users by name, and
systemConfig.additionalFiles by destination path) is not guaranteed to
be stable across runs — the merge implementation uses a map to
deduplicate keys, so the emitted slice order for those two fields may
vary. Every other field’s ordering is stable. The table below lists the
full behavior across a chain:
Section |
Rule (chain semantics) |
|---|---|
|
Non-empty child value overrides; last non-empty level in the chain wins |
|
Replaced wholesale by the child’s value (all four sub-fields together) |
|
Non-nil child pointer replaces the parent value |
|
Wholesale replacement when the child provides a non-empty disk config |
|
Non-empty child overrides |
|
Additive union, deduplicated; each level’s packages are appended in chain order and de-duplicated |
|
Merged by |
|
Merged by |
|
Additive concat: appended in chain order (root first, leaf last), no deduplication |
|
Per-field override: |
|
Per-field override: |
|
Per-field: |
|
Merged only when the child provides some immutability configuration |
|
Merged by |
|
Stripped from the final merged output — it is a build-time directive, not part of the built template |
The rules quoted above are the exact per-field strategies enforced by
MergeConfigurations in internal/config/merge.go.
Overlay mode caveat: when the leaf template uses
baseline.mode: overlay, the additive-packages rule is deliberately overridden. The merged package list becomes exactly the user’s declared packages — the baseline already ships the create-mode toolchain, and unioning it back in would drag in bootloader packages whose strict version pins the frozen baseline cannot satisfy.
Chain Resolution#
At load time, the resolver walks the chain from leaf to root and then folds it back in root-to-leaf order so leaf values have the highest precedence. Once the chain is folded, the OS defaults for the target distribution are applied underneath, producing the effective layering:
OS defaults → root template → intermediate levels → leaf template
Each successive level takes precedence over everything below it. The
merge is applied pairwise: fold(MergeConfigurations, defaults, [root, level1, ..., leaf]).
The build command logs the resolved chain at info level so you can see the inheritance hierarchy directly in build output:
Extends chain: root.yml -> child.yml -> leaf.yml
Worked example. Consider three templates where the leaf declares
packages: [my-custom-app], the middle level declares packages: [prometheus-node-exporter, grafana-agent], the root declares packages: [docker-cli, containerd], and the OS defaults declare
packages: [openssh-server]. The final merged template’s package list
is the union: openssh-server, docker-cli, containerd, prometheus-node-exporter, grafana-agent, my-custom-app (deduplicated,
default order preserved). If any two levels also set kernel.version,
the last non-empty value in the chain wins.
Limitations and Validation Rules#
Single inheritance. Each template may reference at most one parent via
extends:. Multiple inheritance and diamond-shaped graphs are not supported.Cycle detection. The resolver walks the chain with a visited-set keyed on the symlink-resolved canonical absolute path of each template. Any repeat is rejected with
circular extends detected: A -> B -> A. Canonicalizing the path ensures a directory symlink cannot alias two textual paths to the same file and evade the check.Target match. Every template in the chain must share the same
target.os,target.dist,target.arch, andtarget.imageType. A mismatch at any level is rejected withextends target mismatch at level N: child targets os/dist/arch/imageType but parent targets ....Depth warning. Chains that exceed 4 levels emit
extends chain depth N exceeds recommended maximum of 4as a warning. This is a soft cap intended to keep hierarchies maintainable — the build still succeeds.Path containment. The parent path is resolved relative to the child template’s directory. Both a lexical guard and a symlink-resolved guard reject any path that escapes that directory (
extends path escapes child template's directory: ...), so a parent cannot pull a template in from an unrelated location on disk.Symlink rejection. Parent templates that are themselves symbolic links are rejected at load time, matching the same policy the CLI uses for the leaf template.
File extension. Only
.ymland.yamlextensions are accepted; other extensions are rejected.Schema.
extends:is defined as a plain optional string inos-image-template.schema.jsonwith nopatternorformatconstraint — all validation happens in Go at load time so the errors above carry more context than a schema violation would.Output stripping. The
extends:field is stripped from the final merged template because it is a build-time directive, not part of the built image’s declarative state.
Debugging an Extends Chain#
The resolve subcommand renders a template exactly as the build system
sees it after the chain is folded:
# Chain-only merge, without OS defaults (useful for verifying inheritance)
image-composer-tool resolve image-templates/ubuntu24/ubuntu24-x86_64-extends-example-raw.yml
# Full build-time view: OS defaults as base, extends chain folded on top (leaf wins)
image-composer-tool resolve image-templates/ubuntu24/ubuntu24-x86_64-extends-example-raw.yml --full
The output includes the merged systemConfig.packages union and every
other field as it would be used at build time. Sensitive fields (user
passwords, hash_algo values, and secure-boot key/cert paths) are
redacted so the output is safe to paste into an issue or a code review.
See Resolve Command
for the full CLI reference.
See Also#
ADR: template
extends— design rationale and full validation matrixResolve Command — CLI reference for
image-composer-tool resolveTemplate Merge Behavior — the two-layer user↔default merge rules an extends chain reuses at every level
Variable Substitution#
Templates support variable substitution using ${variable_name} syntax. You
can provide variable values via a separate YAML file or command-line flags at
build time.
To learn how variables interact with each build stage, see Build Stages in Detail.
WSL Required Fields#
To compose a WSL-compatible image, set target.imageType: wsl2 and include a
WSL-compatible disk artifact definition.
Field |
Required for WSL |
Requirement |
|---|---|---|
|
Yes |
Standard image identifier |
|
Yes |
Standard image version |
|
Yes |
Any supported OS/distribution with a WSL2 default template |
|
Yes |
Distribution for the selected OS (for example, |
|
Yes |
Use |
|
Yes |
Must be |
|
Yes |
Required when |
|
Yes |
Must be |
|
Yes |
Must be |
Additional notes for WSL builds:
The default Ubuntu WSL template seeds the standard Ubuntu apt sources via
systemConfig.additionalFiles(for example,ubuntu-noble.list), which is the same mechanism used by the raw and initrd defaults.disk.partitionTableTypeanddisk.partitionsare not used forwsl2templates.systemConfig.kernelis not allowed forwsl2templates.
Example disk block for WSL:
disk:
name: ubuntu24-x86_64-agentic
artifacts:
- type: tar
compression: gz
See the full end-to-end example at
image-templates/ubuntu24/ubuntu24-x86_64-agentic-wsl2.yml.
Best Practices#
Start from examples - copy a template from
image-templates/and modify only the fields you need. Let defaults handle the rest.Keep templates minimal - override only what differs from the default. Smaller templates are easier to maintain and review.
Use descriptive names - name images and configs after their purpose (e.g.,
factory-floor-edge, nottest-image-3).Version control your templates - store them in Git alongside your deployment code.
Validate before building - run
image-composer-tool validate template.ymlto catch errors early.Prefer
additionalFilesoverconfigurations- copying config files is more reproducible than running arbitrary shell commands.