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 |
|---|---|
|
|
|
|
Debian package |
|
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-installerbinary. Build it before starting an ISO build:go build -buildmode=pie -o ./build/live-installer ./cmd/live-installerIf 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 |
Example output path |
|---|---|---|
Cloned repo |
|
|
Debian package |
|
|
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 |
|
Delta SBOM (overlay changes only) |
|
Complete SBOM (full final inventory) |
|
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) |
|---|---|---|
|
8 |
8 |
|
|
|
|
|
|
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: trueand the host’s/devmounted. 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’s127.0.0.1only. 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 --buildbuilds the ICT container image frombuild/ict/Dockerfile, which takes several minutes; later runs reuse Docker’s cache. Keep passing--buildafter 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 downanddocker compose up.docker compose down -vdeletes both, and with them every build’s history and images.Volume
Mounted at
Holds
webui-workspace/opt/ict/webui-workspaceEvery build the UI starts: its history entry, log, work directory, downloaded packages, and finished OS image
cache/opt/ict/cacheDownloaded packages for
image-composer-tool buildcommands you run inside the container withdocker compose execA 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 bydocker kill, can leave mounts and loop devices behind on the host.Your proxy. If your shell sets
http_proxy,https_proxy, orno_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"