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.

image-templates

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.

image-templates

Default templates live at:

config/osv/<target.os>/<target.dist>/imageconfigs/defaultconfigs/default-<imageType>-<arch>.yml

Note: imageType: img maps to default-initrd-<arch>.yml (there is no default-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 image and target. 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

description

string

Human-readable description of the template

use_cases

string[]

Use cases this template targets

keywords

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

name

string

Yes

^[a-zA-Z0-9]([a-zA-Z0-9\-_]*[a-zA-Z0-9])?$

Image name (alphanumeric, hyphens, underscores)

version

string

Yes

Semver-like: 1.0.0, 24.04, 1.0.0+build1

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

os

string

Yes

azure-linux, edge-microvisor-toolkit, wind-river-elxr, ubuntu, redhat-compatible-distro

Target operating system

dist

string

Yes

See OS constraints below

Distribution identifier

arch

string

Yes

x86_64, aarch64, armv7hl

Target CPU architecture

imageType

string

Yes

raw, iso, img, wsl2

Output image format

OS → dist constraints:

OS

Valid dist

azure-linux

azl3

edge-microvisor-toolkit

emt3

wind-river-elxr

elxr12

ubuntu

ubuntu24, ubuntu26

redhat-compatible-distro

Any (e.g., el10)

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

mode

string

No

create (default), overlay

Assembly mode

source

object

Yes when mode: overlay (must be absent for create)

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

path

string

one of path/url

Local filesystem path to the baseline image (no URI scheme)

url

string

one of path/url

https:// URL of the baseline image; downloaded over TLS before the overlay runs (plain http is rejected)

format

string

No

Baseline image format: raw, qcow2, vhd or vhdx (default raw). Non-raw formats are converted to RAW before the overlay runs

sbomPath

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

<image-name>-<version>.delta.spdx.json

Always emitted. Only the overlay-induced package changes — the packages the overlay added and, in additive-and-upgrade mode, upgraded.

Complete SBOM

<image-name>-<version>.complete.spdx.json

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:

  1. baseline.source.sbomPath set and valid — that externally-supplied base SBOM is combined with the delta.

  2. sbomPath unset — the SBOM the baseline image itself carries at /usr/share/sbom is used as the base and combined with the delta.

  3. No base SBOM availablesbomPath is 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/sbom manifest 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.configurations commands and systemConfig.additionalFiles are applied, and an optional grow-only resize can enlarge the image to a larger disk.size. Existing baseline packages are not removed unless overlayPolicy.allowPackageRemoval is 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 any extends: chain) with nothing inherited from the OS default. In particular, an overlay template that omits disk.size keeps the baseline’s size — it does not pick up the default’s disk.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 systemConfig sections cannot be applied to an overlay build: hostname, network, initramfs, kernel, immutability, fde, and bootloader. 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 (a create-mode build) instead. The overlay-supported systemConfig inputs are packages, users, configurations, and additionalFiles.

Users in overlay. systemConfig.users is 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’s startupScript must reference a path already present when users are created — i.e. shipped by the baseline or installed by an overlay packages entry — not a file delivered via additionalFiles, which are copied later in the overlay pipeline.

The following systemConfig.users fields are not currently applied (an inherited create-mode limitation, in both create and overlay builds): home, shell, and passwordMaxAge. The login shell is always set to /bin/bash, so a service account cannot yet be pinned to /usr/sbin/nologin via 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.size larger than the baseline and overlayPolicy.allowDiskResize: true. When disk.size is larger but allowDiskResize is not set, the build fails early with a clear message rather than silently resizing the baseline. The resize is grow-only and keyed solely on disk.size (compared to the baseline’s current size), not on how much space the added packages need. If the baseline root is near-full, set disk.size larger than the baseline image and enable allowDiskResize to 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/ext2 roots are the supported and CI-covered target; xfs roots 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 to growpart (cloud-guest-utils), sgdisk (gdisk, GPT only), resize2fs (e2fsprogs) or xfs_growfs (xfsprogs), and losetup/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 via lsblk -o PATH,START,TYPE, which requires util-linux >= 2.38; Ubuntu 22.04’s stock lsblk/resize2fs are too old and not upgradable via apt — 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

packageOperation

string

No

additive-only (default), additive-and-upgrade

Permitted package operations. additive-only: packages may only be added, never removed or downgraded. additive-and-upgrade: also permits upgrading a package already present in the baseline to a newer version. Downgrades and removals remain blocked in both modes (see note below)

conflictPolicy

string

No

fail (default), allow-explicit

How a package conflict detected during preflight is handled. fail aborts the build; allow-explicit permits a conflict only when the conflicting package was explicitly requested

kernelCmdline

string

No

Optional kernel command-line override applied to the overlaid image

allowDiskResize

boolean

No

false (default), true

Permit growing the baseline image to satisfy a larger disk.size. Overlay mode preserves the baseline disk layout by default; when false, a disk.size larger than the baseline is rejected with an error. Resize is always grow-only and never shrinks the image

allowPackageRemoval

boolean

No

false (default), true

Permit removing a baseline package that a to-install package conflicts with (e.g. installing dracut, which conflicts with initramfs-tools). When false (the default) such a conflict fails the build; when true, the conflicting baseline package is removed before install. Only valid with packageOperation: additive-and-upgrade — removal is more invasive than an in-place upgrade, so it is rejected under the default additive-only. Bootloader and bootable-kernel packages are never removed regardless of this flag

additive-and-upgrade scope. 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 of packageOperation. Choose additive-only (the default) to fail the build on any version bump to a baseline package.

Package removal (allowPackageRemoval). Opt-in, and permitted only under packageOperation: additive-and-upgrade — removal is more invasive than an in-place upgrade, so pairing it with the default additive-only is rejected at validation. By default an overlay never removes a baseline package: a to-install package that Conflicts:/Breaks: a present baseline package fails the build. Set allowPackageRemoval: true (with additive-and-upgrade) to let the overlay remove the conflicting baseline package before installing its replacement — the case that makes dracut (which conflicts with initramfs-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

name

string

Yes (schema)

Disk configuration name (e.g., "Default_Raw")

path

string

No

Disk device path (used by live installer, e.g., /dev/sda)

size

string

No

Disk size. Accepts: "4GiB", "8GB", "4096 MiB"

partitionTableType

string

No

gpt or mbr

artifacts

artifact[]

No

Output formats and optional compression

partitions

partition[]

No

Partition layout definitions

disk.artifacts[]#

Each entry defines one output format:

Field

Type

Required

Valid Values

Description

type

string

Yes

raw, qcow2, vhd, vhdx, vmdk, vdi, tar

Output image format

compression

string

No

gz, gzip, xz, zstd, bz2

Compression to apply

disk.partitions[]#

Each entry defines one partition:

Field

Type

Description

id

string

Partition identifier (e.g., boot, rootfs, roothashmap, userdata)

name

string

Partition label

type

string

Partition type (e.g., esp, linux-root-amd64, linux)

typeUUID

string

GPT type GUID (e.g., 8300)

fsType

string

Filesystem type: ext4, fat32, xfs, etc.

fsLabel

string

Filesystem label

start

string

Start offset (e.g., 1MiB, 513MiB)

end

string

End offset (0 means rest of disk)

mountPoint

string

Mount point (e.g., /boot/efi, /, none)

mountOptions

string

Mount options (e.g., defaults, umask=0077)

flags

string[]

Partition flags (e.g., boot, esp, hidden)

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

codename

string

Yes

Repository identifier (e.g., company-internal)

url

string

Yes

Repository base URL (must be a valid URI)

pkey

string

Yes

GPG key URL, absolute file path, or [trusted=yes] to skip verification

component

string

No

Repository component (e.g., main, restricted)

priority

int

No

Priority from -9999 to 9999 (default: 0, higher = preferred)

AllowPackages

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

name

string

No

Configuration name

description

string

No

Human-readable description

hostname

string

No

System hostname

packages

string[]

No

Packages to install (additive with defaults)

kernel

object

No

Kernel configuration

bootloader

object

No

Bootloader configuration

immutability

object

No

dm-verity / Secure Boot configuration

users

user[]

No

User account definitions

initramfs

object

No

Initramfs config (ISO/initrd builds)

additionalFiles

file[]

No

Extra files to copy into the image

configurations

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

version

string

Kernel version (e.g., "6.12", "6.14")

cmdline

string

Kernel boot command line

packages

string[]

Kernel packages (e.g., ["linux-image-generic-hwe-24.04"])

enableExtraModules

string

Additional kernel modules to load

uki

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

bootType

string

efi, legacy

Boot firmware type

provider

string

grub, grub2, systemd-boot

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

backend

string

Yes (when section present)

systemd-networkd, netplan

Network configuration backend

interfaces

object[]

No

See below

List of interface configurations

interfaces[] fields:

Field

Type

Required

Description

name

string

Yes

Interface name (for example, enp1s0, ens3)

dhcp4

bool

No

Enable DHCPv4

dhcp6

bool

No

Enable DHCPv6

addresses

string[]

No

Static addresses in CIDR format

routes

object[]

No

Static routes (see below)

nameservers

string[]

No

DNS server addresses

routes[] fields:

Field

Type

Required

Description

to

string

Yes

Destination (default for default gateway, or CIDR)

via

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 backend is systemd-networkd, the builder enables the systemd-networkd service in the image. When backend is netplan, systemd-networkd is 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

enabled

bool

Yes (when section present)

Enable dm-verity immutable root

secureBootDBKey

string

Conditional

Private key file (.key or .pem)

secureBootDBCrt

string

Conditional

Certificate in PEM format (.crt or .pem)

secureBootDBCer

string

Conditional

Certificate in DER format (.cer)

Note: If any Secure Boot field is provided, all three must be provided and enabled must be true.

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

enabled

bool

Yes

Enable full-disk encryption

passphraseFile

string

Yes (when enabled: true)

Absolute or template-relative local file path containing passphrase material

partitions

string[]

No

Disk partition IDs to encrypt (defaults to the root partition)

unlock

string

No

Boot unlock mode: auto (default) or manual

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

name

string

Yes

Username

password

string

No

Password (plain text or pre-hashed with $ prefix)

hash_algo

string

No

Hash algorithm: bcrypt, sha512, sha256, md5 (md5 is insecure — avoid in production)

passwordMaxAge

int

No

Max password age in days

startupScript

string

No

Script to run on login

groups

string[]

No

Additional groups

sudo

bool

No

Grant sudo permissions

home

string

No

Custom home directory

shell

string

No

Login shell (e.g., /bin/bash)

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

template

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

local

string

Source path on the host (absolute, or relative to template directory)

final

string

Destination path inside the image

stage

string

Overlay-only. When to copy the file relative to initramfs/boot regeneration: "" (default) copies at the end of the build, after regeneration; pre-initramfs copies before boot/initramfs regeneration so the generator can consume it. Ignored by create-mode builds

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: additionalFiles are honored in overlay builds, and each entry’s copy timing is controlled by its stage marker:

  • 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 by update-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-initramfs when the initramfs build must see it; a file that must instead run a generator itself still belongs in a systemConfig.configurations command.

systemConfig.configurations[]#

Shell commands executed inside the chroot during the configuration stage.

Field

Type

Description

cmd

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; supports http:///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 repository

    • 990: prefer over default repositories

    • 1000: 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

image.name, image.version

User overrides default if non-empty

target

User value used entirely

disk

User replaces entire default if non-empty

systemConfig.packages

Additive - user packages appended to defaults (deduplicated)

systemConfig.kernel

User overrides version, cmdline, packages individually if non-empty

systemConfig.bootloader

User overrides individual fields if non-empty

systemConfig.network

User overrides backend if non-empty; interfaces replaced when provided

systemConfig.users

Merged by name - same-name users merged field-by-field; new users appended

systemConfig.additionalFiles

Merged by final path - same destination overrides; new files appended

systemConfig.configurations

Additive - user commands appended after defaults

systemConfig.immutability

Merged only if user explicitly provides the section

systemConfig.fde

Merged only if user explicitly provides the section

packageRepositories

Merged by codename - same codename overrides; new repos appended

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)

image.name, image.version

Non-empty child value overrides; last non-empty level in the chain wins

target

Replaced wholesale by the child’s value (all four sub-fields together)

baseline / overlayPolicy

Non-nil child pointer replaces the parent value

disk

Wholesale replacement when the child provides a non-empty disk config

systemConfig.name / .description / .hostname / .initramfs.template

Non-empty child overrides

systemConfig.packages

Additive union, deduplicated; each level’s packages are appended in chain order and de-duplicated

systemConfig.users

Merged by name: same-name entries are field-level merged; new users included (slice order not guaranteed to be stable across runs)

systemConfig.additionalFiles

Merged by final destination path: same target overrides; new files included (slice order not guaranteed to be stable across runs)

systemConfig.configurations

Additive concat: appended in chain order (root first, leaf last), no deduplication

systemConfig.kernel

Per-field override: version, cmdline, packages, enableExtraModules

systemConfig.bootloader

Per-field override: bootType, provider

systemConfig.network

Per-field: backend overrides if non-empty; interfaces replaced when the child provides them

systemConfig.immutability

Merged only when the child provides some immutability configuration

packageRepositories

Merged by codename: same codename overrides; new codenames appended

extends

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, and target.imageType. A mismatch at any level is rejected with extends 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 4 as 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 .yml and .yaml extensions are accepted; other extensions are rejected.

  • Schema. extends: is defined as a plain optional string in os-image-template.schema.json with no pattern or format constraint — 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#

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

image.name

Yes

Standard image identifier

image.version

Yes

Standard image version

target.os

Yes

Any supported OS/distribution with a WSL2 default template

target.dist

Yes

Distribution for the selected OS (for example, ubuntu24)

target.arch

Yes

Use x86_64 for current WSL2 templates

target.imageType

Yes

Must be wsl2

disk.name

Yes

Required when imageType: wsl2

disk.artifacts[].type

Yes

Must be tar

disk.artifacts[].compression

Yes

Must be gz

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.partitionTableType and disk.partitions are not used for wsl2 templates.

  • systemConfig.kernel is not allowed for wsl2 templates.

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#

  1. Start from examples - copy a template from image-templates/ and modify only the fields you need. Let defaults handle the rest.

  2. Keep templates minimal - override only what differs from the default. Smaller templates are easier to maintain and review.

  3. Use descriptive names - name images and configs after their purpose (e.g., factory-floor-edge, not test-image-3).

  4. Version control your templates - store them in Git alongside your deployment code.

  5. Validate before building - run image-composer-tool validate template.yml to catch errors early.

  6. Prefer additionalFiles over configurations - copying config files is more reproducible than running arbitrary shell commands.