Usage Guide#

A practical guide for common ICT workflows. For the complete command reference, see the CLI Specification.

Table of Contents#


Binary Location#

The path to the image-composer-tool binary depends on how you built or installed it:

Build method

Binary path

go build ./cmd/image-composer-tool

./image-composer-tool

earthly +build

./build/image-composer-tool

Debian package

image-composer-tool (installed to /usr/local/bin/)

The examples below use ./image-composer-tool (the go build location). Substitute the path that matches your setup.

Commands Overview#

image-composer-tool build         # Build an image from a template
image-composer-tool validate      # Validate a template without building
image-composer-tool resolve       # Print the merged template YAML for debugging
image-composer-tool inspect       # Inspect a raw image's structure
image-composer-tool compare       # Compare two images
image-composer-tool ai            # AI-powered template generation (RAG)
image-composer-tool serve         # Run the web UI backend API server
image-composer-tool cache clean   # Manage cached artifacts
image-composer-tool config        # Manage configuration (init, show)
image-composer-tool version       # Display version info
image-composer-tool --help        # Show all commands and options

For the full details on every command — including inspect, compare, and cache — see the CLI Specification.

Building an Image#

ISO images require the live-installer binary. Build it before starting an ISO build:

go build -buildmode=pie -o ./build/live-installer ./cmd/live-installer

If you use earthly +build, both binaries are built automatically. See the Installation Guide for details.

# go build — binary is in the repo root
sudo -E ./image-composer-tool build image-templates/azl3/azl3-x86_64-edge-raw.yml

# earthly +build — binary is in ./build/
sudo -E ./build/image-composer-tool build image-templates/azl3/azl3-x86_64-edge-raw.yml

# Debian package — binary is on PATH
sudo image-composer-tool build /usr/share/image-composer-tool/examples/azl3-x86_64-edge-raw.yml

# Override config settings with flags
sudo -E ./image-composer-tool build --workers 16 --cache-dir /tmp/cache image-templates/azl3/azl3-x86_64-edge-raw.yml

# Build from scratch in throwaway cache/workspace dirs (removed after the build)
sudo -E ./image-composer-tool build --no-cache image-templates/azl3/azl3-x86_64-edge-raw.yml

# Override composition settings for one build (disk strategy, hostname, proxy, SSH keys)
sudo -E ./image-composer-tool build --disk-strategy largest --hostname edge-01 \
  --http-proxy http://proxy.example.com:3128 --ssh-authorized-key admin=$HOME/.ssh/id_ed25519.pub \
  image-templates/ubuntu24/ubuntu24-x86_64-edgepack-unattended-iso.yml

Common flags: --workers, --cache-dir, --work-dir, --no-cache, --verbose, --dotfile, --config, --log-level. Composition overrides: --disk-strategy, --hostname, --http-proxy, --https-proxy, --ftp-proxy, --no-proxy, --ssh-authorized-key. See the full build flag reference for descriptions and additional flags like --system-packages-only.

Build Output#

After the image finishes building, the output is placed under the configured work_dir. The full path follows this pattern:

<work_dir>/<os>-<dist>-<arch>/imagebuild/<system-config-name>/

The default work_dir depends on how you installed the tool:

Install method

Default work_dir

Example output path

Cloned repo

./workspace (relative to repo root)

./workspace/azure-linux-azl3-x86_64/imagebuild/edge/

Debian package

/tmp/image-composer-tool

/tmp/image-composer-tool/azure-linux-azl3-x86_64/imagebuild/edge/

You can override it with --work-dir or by setting work_dir in your configuration file.

An ISO build also leaves the SPDX SBOM of the installed system (spdx_manifest_*.json), a composition manifest (<image-name>-<version>.composition.json), and the merged template (template-dump.yaml) in this directory. See the Unattended ISO Installer Tutorial.

Building an Image in a Docker Container#

Instead of installing the tool and running builds with sudo, you can build inside the ICT container image, which carries image-composer-tool and every program a build runs:

docker build -f build/ict/Dockerfile --target ict -t ict .
scripts/build-in-container.sh my-template.yml ./out

The OS image and its SBOM land in ./out/<system-config-name>/, owned by you. The container runs privileged with the host’s /dev mounted, so it is root-equivalent on the host. The CLI Quick Start (Docker) covers writing a template, the mounts, and every option of the script.

The script wraps one docker run, which runs /opt/ict/scripts/container-build.sh inside the container. To run that yourself, mount the template’s directory and your output directory, and pass the output’s mount point as --output; it must be a mounted volume, or the files would disappear with the container. Create the output directory first, so it is not owned by root:

mkdir -p out
docker run --rm --init --privileged -v /dev:/dev \
  -v "$PWD:/input:ro" -v "$PWD/out:/artifacts" \
  ict /opt/ict/scripts/container-build.sh \
    --template /input/my-template.yml --output /artifacts --owner "$(id -u):$(id -g)"

Comparing Overlay Outputs#

An overlay build layers packages onto an existing baseline RAW image and emits, alongside the result, SPDX SBOM sidecars (see SBOM generation (overlay mode)):

Artifact

Name

Overlay RAW image

<name>-<version>.raw

Delta SBOM (overlay changes only)

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

Complete SBOM (full final inventory)

<name>-<version>.complete.spdx.json (only when a base SBOM is available; otherwise skipped)

The existing compare command works directly against these outputs — no overlay-specific flags are needed. The examples below assume the baseline is at baseline.raw and the overlay build produced myimage-1.0.* under the build output directory.

Baseline RAW vs Overlay RAW#

Compare the structural/binary differences between the baseline and the overlay result. Add --hash-images to compute SHA256 and get a binary-identity verdict:

# Human-readable structural diff (partition table, filesystems, bootloader, SBOM metadata)
./image-composer-tool compare baseline.raw myimage-1.0.raw

# High-level counts as JSON, suitable for CI
./image-composer-tool compare --format=json --mode=summary baseline.raw myimage-1.0.raw

# Binary-identity check (slower — hashes both images)
./image-composer-tool compare --hash-images baseline.raw myimage-1.0.raw

Expected output pattern (text): a top Equality: verdict followed by the changed sections. Because the overlay grows the disk and rewrites the embedded SBOM, the verdict is different, and the summary reports sbomChanged and a size change:

Equality: different (meaningful diffs: ...)
Image:
  Size: 4.0 GiB -> 6.0 GiB
SBOM:
  PackageCount: 120 -> 123

The equality classes are binary_identical (only with --hash-images, when the SHA256s match), semantically_identical / semantically_identical_unverified (no meaningful diffs), or different.

Note: In the default image modes the compare tool reports SBOM metadata (package count, canonical hash) only. For a package-by-package breakdown, use the SBOM comparison below.

SBOM Comparison (Complete SBOM)#

Use --mode=spdx for an accurate package-level diff. The complete SBOM represents the full final image inventory, so comparing the baseline’s SBOM against the overlay complete SBOM reports exactly which packages were added or upgraded:

# Compare the baseline SBOM against the overlay complete SBOM
./image-composer-tool compare --mode=spdx baseline.spdx.json myimage-1.0.complete.spdx.json

# Either argument may also be an image — its embedded SBOM (/usr/share/sbom) is
# extracted automatically, so you can compare the baseline RAW directly:
./image-composer-tool compare --mode=spdx baseline.raw myimage-1.0.complete.spdx.json

# JSON for tooling
./image-composer-tool compare --format=json --mode=spdx baseline.spdx.json myimage-1.0.complete.spdx.json

Expected output pattern (text): added, removed, and upgraded package lists. A same-name version bump is reported as a single upgrade (~), not a remove + add:

SPDX Compare
============
Equal:    false
Packages: 120 -> 123

Added packages:
  + tree|2.1.1|...

Upgraded packages:
  ~ curl: 8.5.0 -> 8.6.0

Overlay builds are additive by default, so you will typically see additions and upgrades. Removals appear only when overlayPolicy.allowPackageRemoval is enabled (a conflict-driven baseline package removal).

Delta SBOM: Optional Quick-Change View#

The delta SBOM lists only the overlay-contributed packages (what changed). It is an optional convenience for a quick “what did this overlay add?” view without diffing against the baseline — you can inspect it directly or diff it against an empty document:

# The delta SBOM is a plain SPDX JSON file — read it directly
jq '.packages[].name' myimage-1.0.delta.spdx.json

# Or diff it against an empty SBOM to render the same added-package list via compare
echo '{"packages":[]}' > empty.spdx.json
./image-composer-tool compare --mode=spdx empty.spdx.json myimage-1.0.delta.spdx.json

For an authoritative before/after picture of the image, prefer the complete SBOM comparison above; the delta is a shortcut, not a replacement.

Validating a Template#

Check a template for errors before starting a build:

./image-composer-tool validate image-templates/azl3/azl3-x86_64-edge-raw.yml

Resolving a Template#

Print the merged template YAML to stdout — useful for debugging templates that use extends: or for previewing exactly what the tool will build. Sensitive fields (user passwords, systemConfig.users[*].hash_algo, the secure boot signing paths systemConfig.immutability.secureBootDBKey, systemConfig.immutability.secureBootDBCrt, and systemConfig.immutability.secureBootDBCer, and the DKMS secure boot signing paths systemConfig.dkms.secureBoot.signingKeyPath, systemConfig.dkms.secureBoot.signingCertPath, systemConfig.dkms.secureBoot.targetKeyPath, and systemConfig.dkms.secureBoot.targetCertPath) are always redacted in the output:

# Chain-merge only, without OS defaults
./image-composer-tool resolve image-templates/ubuntu24/ubuntu24-x86_64-extends-example-raw.yml

# Full build-time view: extends chain + OS defaults
./image-composer-tool resolve image-templates/azl3/azl3-x86_64-edge-raw.yml --full

Debugging an Extends Chain#

When a template uses extends: to inherit from a parent, resolve folds the chain and prints the effective template so you can verify what the leaf actually inherits before running a build:

# Show what an extends child inherits from its parent
./image-composer-tool resolve image-templates/ubuntu24/ubuntu24-x86_64-extends-example-raw.yml

The output includes the union of the parent’s and child’s systemConfig.packages, plus every other field folded through the chain’s per-section merge rules. OS defaults are not applied unless you pass --full, so the output reflects only what the templates in the chain declared — useful for spotting an accidentally-overridden field. For the complete inheritance semantics (single-parent chains, cycle detection, target-match rules, and the depth-warning threshold), see Template Extends (Inheritance).

Configuration#

The tool uses a layered configuration: config file values are overridden by command-line flags. A config file is auto-discovered from several standard locations (current directory, home directory, /etc/), or you can specify one explicitly with --config.

# Create a default configuration file
./image-composer-tool config init

# Show the active configuration
./image-composer-tool config show

# Use a specific configuration file
./image-composer-tool --config /path/to/config.yaml build template.yml

Key settings:

Setting

Default (cloned repo)

Default (Debian pkg)

workers

8

8

cache_dir

./cache

/var/cache/image-composer-tool

work_dir

./workspace

/tmp/image-composer-tool

For the complete search order and all configuration fields, see Configuration Files in the CLI Specification.

Operations Requiring Sudo#

The build command requires sudo because it performs system-level operations: creating loop devices, mounting filesystems, setting up chroot environments, installing packages, and configuring bootloaders.

Always run builds with sudo -E to preserve your environment variables (such as $PATH and proxy settings).

Running the Web UI#

serve starts the HTTP API behind the web UI and serves the frontend bundle embedded in the binary. Run it from the repository root — it resolves --templates-dir, --work-dir, and the configuration file relative to its working directory:

# Start on http://127.0.0.1:8080
./image-composer-tool serve

# Pick a different port
./image-composer-tool serve --port 9090

Builds still need root. Run the server as root on an isolated build host, or run it as an unprivileged user with --sudo plus the scoped sudoers rules that let it cancel builds and read root-owned artifacts. Generate and install those rules rather than writing them by hand:

# Generate, visudo-validate, and install in one step
scripts/install-sudoers.sh

# Then run builds under sudo -n
./image-composer-tool serve --sudo

The server binds to localhost by default and has no authentication of its own. Because the API can start privileged builds, exposing it with --host 0.0.0.0 should be a deliberate decision, not a convenience.

The OS manifest, the repository catalog, and the Edge Pack catalog are embedded at build time. To edit any of them and see the change after a restart instead of a rebuild, point the matching flag at a file on disk:

./image-composer-tool serve \
  --package-repos ./internal/api/service/data/package-repos.yaml \
  --edge-pack ./internal/api/service/data/edge-pack.yaml

A malformed or missing override file stops the server at startup rather than falling back to the embedded copy.

For every flag, the sudoers rules in full, and their security implications, see the Serve Command reference. For building the frontend bundle and the hot-reload development loop, see web/README.md.

Running the Web UI With Docker Compose#

Instead of building the binary and installing sudoers rules, you can run the web UI in a container. The host needs only Docker with the Compose plugin: the ICT container image, which Docker builds from your checkout, carries the binary, the web UI, and every program a build runs. From the root of a checkout:

docker compose up --build

Then open http://127.0.0.1:8080. The Web UI Quick Start walks through this step by step, including where to find the OS images you build.

Warning: the container is privileged, and therefore root-equivalent on the host. Building an OS image creates loop devices, mounts filesystems, and chroots into them, so the container runs with privileged: true and the host’s /dev mounted. Every build runs as root with access to all of the host’s devices, and the web UI has no authentication of its own: anyone who can reach the port can start a build. The port is therefore published on the host’s 127.0.0.1 only. Do not change that binding to expose the UI on a network, and run the container only on a machine where you would trust everyone who can reach the UI with root.

What docker-compose.yml sets up:

  • The container image. The first docker compose up --build builds the ICT container image from build/ict/Dockerfile, which takes several minutes; later runs reuse Docker’s cache. Keep passing --build after you pull new commits, so the image picks them up.

  • Two named volumes. Docker stores them outside your checkout and keeps them across docker compose down and docker compose up. docker compose down -v deletes both, and with them every build’s history and images.

    Volume

    Mounted at

    Holds

    webui-workspace

    /opt/ict/webui-workspace

    Every build the UI starts: its history entry, log, work directory, downloaded packages, and finished OS image

    cache

    /opt/ict/cache

    Downloaded packages for image-composer-tool build commands you run inside the container with docker compose exec

  • A clean stop. docker compose down, or Ctrl+C, asks the server to stop. It cancels a running build and waits for the build to unmount its filesystems and detach its loop devices; the compose file allows 60 seconds for that. As with an interrupted build outside a container, a build killed without that cleanup, for example by docker kill, can leave mounts and loop devices behind on the host.

  • Your proxy. If your shell sets http_proxy, https_proxy, or no_proxy (or their uppercase forms), Compose passes them to the container image build and to the server, which passes them on to every build.

The container also has the image-composer-tool CLI. Its working directory, /opt/ict, is laid out like a checkout of this repository, so paths such as image-templates/ work as they do in your checkout:

docker compose exec ict image-composer-tool validate image-templates/ubuntu24/ubuntu24-x86_64-minimal-raw.yml

To build OS images from the command line without the web UI, see Building an Image in a Docker Container.

To build the container image without Compose, run docker build -f build/ict/Dockerfile --target ict -t ict ., or earthly +ict-image, which also records the version and git commit in it (--VERSION=<tag> sets the version and the image tag, --ICT_IMAGE_NAME=<name> the image name). earthly +ict-image-preflight also checks that every program ICT may run is present in the image.

Shell Completion#

# Auto-detect shell and install completion
./image-composer-tool install-completion

# Or specify a shell: bash, zsh, fish, powershell
./image-composer-tool install-completion --shell bash

After installing, reload your shell configuration (e.g., source ~/.bashrc). For per-shell activation steps and manual completion script generation, see the Install-Completion Command reference.

Template Examples#

Templates are YAML files that define the requirements for an image build. For the full template system documentation, see Creating and Reusing Image Templates.

The image-templates/ directory contains ready-to-use examples for all supported distributions and image types.

Minimal Edge Device#

image:
  name: minimal-edge
  version: "1.0.0"

target:
  os: azure-linux
  dist: azl3
  arch: x86_64
  imageType: raw

systemConfig:
  name: minimal
  description: Minimal edge device configuration
  packages:
    - openssh-server
    - ca-certificates
  kernel:
    version: "6.12"
    cmdline: "quiet"

Development Environment#

image:
  name: dev-environment
  version: "1.0.0"

target:
  os: azure-linux
  dist: azl3
  arch: x86_64
  imageType: raw

systemConfig:
  name: development
  description: Development environment with tools
  packages:
    - openssh-server
    - git
    - docker-ce
    - vim
    - curl
    - wget
    - python3
  kernel:
    version: "6.12"
    cmdline: "quiet splash"

Edge Microvisor Toolkit#

image:
  name: emt-edge-device
  version: "1.0.0"

target:
  os: edge-microvisor-toolkit
  dist: emt3
  arch: x86_64
  imageType: raw

systemConfig:
  name: edge
  description: Edge Microvisor Toolkit configuration
  packages:
    - cloud-init
    - rsyslog
  kernel:
    version: "6.12"
    cmdline: "console=ttyS0,115200 console=tty0 loglevel=7"