Build an Ubuntu image¶
In this tutorial, we’ll build a custom Ubuntu image for AMD64 machines. We’ll work through everything from the initial project setup to the image’s first boot.
The tutorial takes about 25 minutes to complete. It doesn’t require an intimate understanding of disk images, but you’ll need to be familiar with Linux paradigms and using the terminal.
What we’ll build¶
After installing the necessary tools, we’ll start building a custom Ubuntu image from the ground up.
We’ll define the image’s structure and content step by step. The image will be based on the suite of packages from Ubuntu 24.04 LTS, with some additional software that caters it to the tutorial.
We’ll end the tutorial by packaging the complete image and running it with QEMU, a popular machine emulator.
Once you’ve completed the tutorial, you’ll have practical experience with Imagecraft and a custom image you can add software to or model your next image from.
What you’ll need¶
For this tutorial, you’ll need:
An AMD64 machine running Ubuntu 24.04 LTS
Super user privileges on your machine
10GiB of available storage
Install prerequisites¶
To begin, let’s install the Imagecraft snap. Open a terminal and run:
snap install imagecraft --beta --classic
Next, let’s install Multipass, which will create the build environment when it comes time to package the image:
snap install multipass
We’ll run our image with QEMU. Install it with:
sudo apt install qemu-system-x86
Lastly, we’ll need UEFI firmware to pass to QEMU. One of the most popular choices is OVMF. Install it with:
sudo apt install ovmf
Set up the project¶
We’ll need a directory to hold the project. Create a directory wherever you like to keep your software projects:
mkdir ubuntu-minimal
cd ubuntu-minimal
Images are built and configured through an imagecraft.yaml file, called the project
file. Let’s create one in the new project directory with the init command:
imagecraft init
Open the generated project file in your preferred text editor. This is where most of the tutorial will take place.
Describe the image¶
An image’s project file starts with details like its name, version, and build
environment. Imagecraft initialized these keys with generic values. Let’s update the
summary and description keys to better reflect the new project. Replace the
first six keys with:
name: ubuntu-minimal
base: bare
build-base: ubuntu@24.04
version: '0.1'
summary: A lightweight, unofficial Ubuntu image for AMD64 machines.
description: |
The ubuntu-minimal image is a lightweight, unofficial Ubuntu image for AMD64
machines. Its root file system and packages are based off of Ubuntu 24.04 LTS,
and it's booted with GRUB.
The base key defines the files that make up the foundation of the image. We’re
starting with an empty directory, known as the bare base, and building it up from
scratch.
The build-base key defines the operating system that’s used to assemble the image.
It does not affect the image’s contents. It’s best to build with the latest Ubuntu LTS
release in most cases, so we left this unchanged.
The summary and description keys tell consumers of the image a little more
about it. The summary is a one-line description, limited at 79 characters, while the
description is more open-ended and can span multiple lines. These were both placeholders
in the template project file, so we made them meaningful for this project.
Define the partitions¶
Now that we’ve described the image and declared its build details, we need to define its
partitions. To do so, we’ll customize the volumes key.
The volumes key contains a single entry, named disk. The schema key tells us
that disk is partitioned with GPT, the recommended schema for most images.
We’ll define individual partitions in the structure key.
The image will have two partitions: a root file system and an EFI system partition. Each
will need their own entry in the structure key. The first was defined for us
automatically. Before we go over it, let’s define the EFI system partition the image
will boot from. Add the following highlighted lines after the rootfs partition:
There’s a lot to unpack here. Let’s take a moment to go over each of the efi
partition’s keys and compare them to the rootfs partition.
We declared that this is the boot partition by setting the role key to
system-boot. The rootfs partition’s role key was set to system-data,
which tells Imagecraft that it contains the operating system.
We set the type key to the identifier for EFI system partitions in a GUID partition
table. The rootfs partition was generated with the identifier for Linux file
systems. The identifiers themselves come from the UEFI specification—don’t worry about
memorizing them.
We set the filesystem key to vfat for its compatibility with EFI system
partitions. Since the rootfs partition will be for general usage, it uses the ext4
filesystem instead.
In both partitions, the filesystem-label key is set to a unique, human-readable
name. This is for the benefit of you and anyone else who might work with the packaged
image later on.
Mount the partitions¶
The image’s partitions are ready, but we haven’t told Imagecraft where to mount them in
the image’s file system. Let’s shift our focus to the filesystems key.
The filesystems key maps the image’s partitions to their mount points. It expects a
single file system, named default, that mounts a partition to the root of the image.
The filesystems key in the project file already mounts the rootfs partition to the
image’s root, but we’ll still need to mount the EFI system partition.
Add the following highlighted lines to the end of the filesystems key:
With this entry, we mounted the EFI system partition to the /boot/efi/ directory in the final image. We’ll create this directory shortly.
Set up the root file system¶
Because we’re building the image on the bare base, its file system is currently an empty
directory. Let’s start building it up with the parts key.
Parts are the means by which we source packages for and manipulate the files in the image. They’re the primary way we interact with the overlay file system, which is where we’ll build up the image.
We’ll create the file system with a part that uses the mmdebstrap plugin.
In the parts key, replace the template part with a new part named rootfs,
defined as follows:
parts:
rootfs:
plugin: mmdebstrap
mmdebstrap-suite: noble
The mmdebstrap-suite key is specific to the mmdebstrap plugin and specifies the
package suite to bootstrap. To get the packages that are shipped with Ubuntu 24.04 LTS,
we set the suite to noble.
The plugin removes the default sources configuration files, which limit us to the system
packages from the noble suite’s main component. If we want to install anything
more than essential system packages, we’ll need to add a new sources configuration file.
We also still need to create the /boot/efi/ directory we mounted the efi partition
to.
Add the following override-build key to the part:
The override-build key replaces the plugin’s default behavior. Since we want to
extend the part’s build instead of overriding it, we started the script with craftctl
default, which runs the plugin’s default commands.
Now, when we install packages into the image, we’ll be able to access the other
components in the noble suite.
At this point, the file system only exists in the rootfs part. To get it into the
final image, we’ll need to copy it into the overlay file system. We can do so with the
organize key and the (overlay)/ prefix. Add the following highlighted lines to
the rootfs key:
This copies the result of the part’s build step to the root of the overlay file system, thereby securing its place in the final image.
Add essential packages¶
We’ll need some additional packages for the image to be bootable. Let’s define a new
part to source them. In this case, we don’t need any special behavior, so we’ll set the
plugin key to nil. Add a new part named packages, defined as follows:
packages:
plugin: nil
overlay-packages:
- ubuntu-server-minimal
- linux-image-generic
- grub2-common
- grub-pc
- shim-signed
- sl
With the exception of sl, these packages add system essentials such as the kernel,
core utilities, and boot loader. You won’t need to worry about sl until we run the
image later on.
Create the file system table¶
If we tried to boot the image now, its partitions wouldn’t be mounted. This is because
Imagecraft requires that we create our file system table manually. The content of this
table is similar to what we declared in the filesystems key, with some additional
configuration.
With how we set up the partitions and mount points, the table will read:
The first three columns should look familiar—these are the labels, mount points, and file system types we declared for our partitions. The last three columns declare each partition’s active mount options, whether we want to dump the partition’s utility backup, and the file system check order.
Let’s create a part that writes this to the /etc/fstab/ directory in the overlay
file system. Add a new part named fstab, defined as follows:
fstab:
plugin: nil
overlay-script: |
cat << EOF > $CRAFT_OVERLAY/etc/fstab
LABEL=root / ext4 discard,errors=remount-ro 0 1
LABEL=uefi /boot/efi/ vfat umask=0077 0 1
EOF
Here, we used the overlay-script key to write the table to the overlay file system,
which is referenced through the $CRAFT_OVERLAY environment variable. Keep in mind
that this environment variable is only available in parts that include, or depend on
another part that includes, overlay keys.
The partitions will now be mounted automatically when the system boots.
Set the default user¶
To interact with the system after we boot the image, we’ll need to set the default user and password.
For the purposes of this tutorial, we’ll set up a login part that runs the
chpasswd command in the overlay file system. This should not be done in images
built for production environments.
Add a new part named login, defined as follows:
login:
plugin: nil
overlay-script:
echo "root:password" | chpasswd --root "${CRAFT_OVERLAY}"
When we run our image later, we’ll log in with the username root and the password
password.
Our project file now contains everything we need to pack a complete, bootable image.
Save and close the imagecraft.yaml file.
Pack the image¶
To isolate the image build from your machine, we’ll pack the image in a Multipass VM.
Open a new terminal in the ubuntu-minimal/ project directory and run:
snap set imagecraft provider=multipass
imagecraft pack
The packing process takes around ten minutes. When your terminal shows the following line, the build is complete:
Packed disk.img
Congratulations on building your first image! Before you start celebrating, let’s run the image to make sure everything is working as expected.
Run and test the image¶
Before we run our image with QEMU, let’s copy the UEFI variables from OVMF into a temporary directory so we don’t compromise the originals:
cp /usr/share/OVMF/OVMF_VARS_4M.fd /tmp/OVMF_VARS_4M.fd
You’ll need to repeat this step if you reboot your machine between runs.
With no further ado, let’s run the image with QEMU:
qemu-system-x86_64 \
-accel kvm \
-m 4G \
-cpu host \
-smp 8 \
-drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd \
-drive if=pflash,format=raw,file=/tmp/OVMF_VARS_4M.fd \
-drive file=disk.img,format=raw,index=0,media=disk
This will open QEMU in a separate window. After about a minute, it’ll display the following login prompt:
imagecraft-ubuntu-minimal-amd64-49807517 login:
As you may recall from the login part, the default username is root and the password
is password. Enter these into the QEMU shell now.
By booting and logging in to the image, we’ve verified the presence of its essential
packages. To ensure the packages from the extra components of the noble suite are in
place, let’s run the sl command in the QEMU shell.
==== ________ ___________
_D _| |_______/ \__I_I_____===__|_________|
|(_)--- | H\________/ | | =|___ ___| _________________
/ | | H | | | | ||_| |_|| _| \\_____A
| | | H |__--------------------| [___] | =| |
| ________|___H__/__|_____/[][]~\_______| | -| |
|/ | |-----------I_____I [][] [] D |=======|__ __|________________________|_
__/ =| o |=-~~\ /~~\ /~~\ /~~\ ____Y___________|__ |__________________________|_
|/-=|___|= || || || |_____/~\___/ |_D__D__D_| |_D__D__D_|
\_/ \O=====O=====O=====O_/ \_/ \_/ \_/ \_/ \_/
Review the project file¶
Here’s the complete project file for the image. Yours should look similar to it.
imagecraft.yaml for ubuntu-minimal
name: ubuntu-minimal
base: bare
build-base: ubuntu@24.04
version: '0.1'
summary: A lightweight, unofficial Ubuntu image for AMD64 machines.
description: |
The ubuntu-minimal image is a lightweight, unofficial Ubuntu image for AMD64
machines. Its root file system and packages are based off of Ubuntu 24.04 LTS,
and it's booted with GRUB.
platforms:
amd64:
volumes:
disk:
schema: gpt
structure:
- name: rootfs
role: system-data
type: 0FC63DAF-8483-4772-8E79-3D69D8477DE4
filesystem: ext4
filesystem-label: root
size: 3G
- name: efi
role: system-boot
type: C12A7328-F81F-11D2-BA4B-00A0C93EC93B
filesystem: vfat
filesystem-label: uefi
size: 256M
filesystems:
default:
- device: (volume/disk/rootfs)
mount: /
- device: (volume/disk/efi)
mount: /boot/efi/
parts:
rootfs:
plugin: mmdebstrap
mmdebstrap-suite: noble
override-build: |
craftctl default
mkdir $CRAFT_PART_INSTALL/boot/efi/
cat << EOF > $CRAFT_PART_INSTALL/etc/apt/sources.list.d/ubuntu.sources
Types: deb deb-src
URIs: http://archive.ubuntu.com/ubuntu
Suites: noble noble-updates noble-backports noble-security
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
EOF
organize:
'*': (overlay)/
packages:
plugin: nil
overlay-packages:
- ubuntu-server-minimal
- linux-image-generic
- grub2-common
- grub-pc
- shim-signed
- sl
fstab:
plugin: nil
overlay-script: |
cat << EOF > $CRAFT_OVERLAY/etc/fstab
LABEL=root / ext4 discard,errors=remount-ro 0 1
LABEL=uefi /boot/efi/ vfat umask=0077 0 1
EOF
login:
plugin: nil
overlay-script:
echo "root:password" | chpasswd --root "${CRAFT_OVERLAY}"
Conclusion¶
This marks the end of this image’s journey. If you’d like to develop your crafting skills further, you can customize the image or even build a new one from scratch.
If you create an image for a new system or architecture, we encourage you to share it with us on Matrix. We’d love to see what you come up with.
If you’d like to share any feedback on Imagecraft or this tutorial, please open an issue. We appreciate your input.