This project outlines the steps to create an Ubuntu root filesystem (rootfs) tailored for the Kria platform, configure it for NFS booting, and set up the necessary environment both on the host PC and the Kria device.
By the end of this guide, you’ll also have a bootable Ubuntu 22.04 LTS SD card image built from scratch using the Chroot tools on your host computer, completely inside Docker.
PrerequisitesA host computer with the following services properly configured and running:
- TFTP server (version: 5.2+20150808-1.4build1 or later)
- NFS server (version: 1:2.6.4-3ubuntu5.1 or later)
- Docker Engine (version: 28.1.1, build 4eba377)
This tutorial builds on the previous three tutorials:
- Project 1: Run PetaLinux in Docker: Isolated Development
- Project 2: Build a Custom PetaLinux Image for Kria Board
- Project 3: Boot Kria Over Network Using TFTP and NFS Servers
Note: You will need the following components build in Project 2:
- Linux kernel:
Image
- Device Tree Blob:
system.dtb
In this project, we’re building a fully functional Ubuntu Linux environment for the Kria KV260 platform — starting from scratch. The goal is to prepare a minimal ARM64 Ubuntu root filesystem, customize it for embedded development, and make it bootable over NFS or from an SD card using a custom-built kernel and device tree from a PetaLinux project.
Instead of doing this directly on the Kria board, which would be slow and cumbersome, we perform all preparation and customization steps on a host PC using a Docker container that emulates the ARM64 architecture. This allows us to install packages, configure the system, and even simulate boot-time behavior using chroot.
Why use Docker and ARM64 emulation?
The root filesystem we prepare is for a 64-bit ARM architecture (aarch64), which differs from the x86_64 architecture of most development PCs. To work with this filesystem natively, we need to run ARM64 binaries — something that’s not possible directly on an x86 system. Docker, paired with QEMU, lets us build and interact with this ARM64 rootfs environment safely and reproducibly.
What is chroot
and why use it?
chroot
(short for change root) lets us simulate being “inside” the root filesystem we’re building — as if we had booted into it on the Kria board. This lets us install packages, generate locales, set timezones, enable services, and make system-level changes — before the system ever boots on real hardware.
What we’ll build
By the end of this project, you will have:
- A customized, bootable Ubuntu rootfs for ARM64
- A kernel and DTB compiled with PetaLinux from your hardware design (XSA)
- A working NFS setup or a bootable SD card image for Kria
- A deep understanding of how to assemble a Linux system for embedded devices using only your PC
Let’s get started!
Part 1: Preparing the Ubuntu Rootfs on Host PCFor the root filesystem base, we use the official Ubuntu Base 22.04 for ARM64, which is a minimal tarball provided by Canonical specifically for embedded and custom system builds. It contains just enough of Ubuntu to boot and be functional, making it a perfect starting point for customization. You can download it directly from Canonical’s servers, as shown below.
1.1. Prepare the Environment and Download Ubuntu Base Rootfs
- Create a working directory. All subsequent commands will be run from this location unless stated otherwise:
mkdir -p kria-ubuntu/workspace
cd kria-ubuntu/workspace
- Download the Ubuntu base rootfs for ARM64 architecture:
wget http://cdimage.ubuntu.com/ubuntu-base/releases/22.04/release/ubuntu-base-22.04-base-arm64.tar.gz
- Extract the rootfs to a designated sub directory in the workspace:
mkdir -p ./mnt/rootfs
tar -xpf ubuntu-base-22.04-base-arm64.tar.gz -C ./mnt/rootfs
1.2. Set Up QEMU for ARM64 Emulation
Register QEMU to enable ARM64 emulation. Use this command only once:
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
This command sets up QEMU user-mode emulation support within Docker, enabling the host system (typically x86_64) to transparently run containers built for different CPU architectures—like ARM64. By registering QEMU static binaries with the Docker engine (--reset -p yes
), we ensure that when an ARM64 container is run, Docker can automatically emulate the ARM64 instruction set using QEMU. This is essential for building and interacting with ARM64 Ubuntu rootfs images on a standard development PC that doesn’t natively support that architecture.
1.3. Create Docker Builder for ARM64
Set up a Docker builder that supports ARM64:
docker buildx create --name kria-builder --use
This command creates and activates a new Docker builder instance named kria-builder
using the buildx extension.
Docker Buildx is an extended build tool that supports advanced features like cross-compilation and multi-platform builds. In our case, it enables building ARM64-compatible Docker images on an x86_64 host machine by leveraging QEMU emulation behind the scenes.
This step is crucial because the default Docker builder does not support cross-platform builds natively, and we need to build and test the Ubuntu rootfs environment for the ARM64-based Kria board directly from our development PC.
To verify that the Docker builder named kria-builder
is set up correctly and currently active, run the following command:
docker buildx ls
The output should display an asterisk (*) next to kria-builder
, indicating it is the active builder instance.
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
kria-builder* docker-container
\_ kria-builder0 \_ unix:///var/run/docker.sock inactive
default docker
\_ default \_ default running v0.21.0 linux/amd64 (+4), linux/arm64, linux/arm (+2), linux/ppc64le, (7 more)
Then, build a Docker image for ARM64. Use the provided Dockerfile.arm64
file from the Attachment section.
cp /path/to/Dockerfile.arm64 .
docker buildx build --platform linux/arm64 -f Dockerfile.arm64 -t ubuntu-arm64:kria --load .
The command builds a Docker image using the buildx system specifically targeting the ARM64 architecture. It uses the provided Dockerfile.arm64, which is based on the official Ubuntu 22.04 ARM64 image.
Inside this Dockerfile, we install essential utilities, configure system locales, set up a non-root user (kria
), and prepare the image for running commands in an ARM64 Ubuntu environment.
The --platform linux/arm64
flag ensures the image is compatible with Kria’s ARM64 architecture, while --load
makes the built image immediately available in the local Docker image list (rather than pushing it to a registry).
The resulting image acts as a lightweight ARM64 Ubuntu environment that can be used on an x86_64 host PC to prepare and chroot into a root filesystem for Kria—effectively emulating the board’s architecture without needing the hardware for these early steps.
1.4. Launch Docker Container with Mounted Rootfs
Run the Docker container with the rootfs mounted:
docker run --rm -it --platform linux/arm64 \
--privileged \
-v ./mnt/rootfs:/mnt/rootfs \
ubuntu-arm64:kria
While inside the Docker container, verify that the rootfs is properly mounted by listing its contents:
ls /mnt/rootfs.
You should see output similar to:
root@391b6d525d2c:/home/kria/workspace# ls /mnt/rootfs
bin boot dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
root@391b6d525d2c:/home/kria/workspace#
Part 2: Customizing and Chrooting into the Rootfs2.1. Mount Pseudo Filesystems
Before chrooting (in the Docker container as a root user), it’s essential to mount certain pseudo filesystems—such as /proc, /sys, /dev, and /run
—into the target rootfs directory to provide the chroot environment with critical access to kernel and device interfaces.
These mounts effectively mirror the host system’s live interfaces into the rootfs, allowing processes inside the chroot to interact with system-level resources like process information, system hardware, devices, and runtime sockets.
Without these mounts, many standard utilities inside the chroot (like apt
, systemctl
, or even ls /proc
) would fail or behave unpredictably because the expected system context is missing.
This step ensures that the chrooted Ubuntu environment behaves as closely as possible to a fully booted system, which is particularly important when configuring systemd-based services or installing packages that rely on system introspection.
mount --bind /dev /mnt/rootfs/dev
mount --bind /dev/pts /mnt/rootfs/dev/pts
mount --bind /proc /mnt/rootfs/proc
mount --bind /sys /mnt/rootfs/sys
Alternatively, you can use the provided mount-chroot-ro.sh
script from the Attachments section. This is especially useful if you need to repeat the process multiple times—for example, to install additional packages incrementally.
./mount-chroot-ro.sh
2.2. Configure DNS Resolution
Configure DNS resolution inside the chroot environment. Note that this step is already handled by the included mount-chroot-ro.sh
script.
rm /mnt/rootfs/etc/resolv.conf
echo "nameserver 8.8.8.8" | tee /mnt/rootfs/etc/resolv.conf
cat /mnt/rootfs/etc/resolv.conf
Note: Skipping this step will result in non-functional DNS inside the chroot environment, causing commands like apt update
to fail. Repeat this step each time you try chroot into this rootfs.
2.3. Chroot into the Rootfs
Enter the chroot environment using the command below.
This effectively “logs you in” to the mounted Ubuntu root filesystem as if it were your actual system. From this point onward, any commands you run will be executed inside the rootfs environment, allowing you to install packages, configure settings, and prepare the filesystem exactly as it will behave when booted on the Kria board.
chroot /mnt/rootfs /bin/bash
2.4. Install Essential Packages
Within the chroot, update package lists and install essential packages.
Once inside the chrooted environment, begin by updating the package index and installing the minimal required packages to make the system usable:
apt update
apt install libc-bin systemd udev
Why systemd
is important:
systemd
is the init system used by modern Linux distributions, including Ubuntu. It is responsible for booting the system, managing services, logging, and handling tasks like device mounting, hostname setup, and more. Without it, your system wouldn’t start properly on the Kria board or handle networking and service management correctly.
Then, install a full minimal Ubuntu setup:
unminimize
apt install ubuntu-standard
The unminimize
command restores essential packages that are omitted in the minimal image — such as man pages, additional localization files, and useful utilities — making the system behave more like a standard Ubuntu install.
Afterward, ubuntu-standard
ensures your rootfs includes the core Ubuntu tools typically expected in a server environment.
You can now optionally install tools you’ll want on your system:
apt install sudo tzdata vim git iproute2 iputils-ping net-tools dnsutils less kmod openssh-server locales
This step prepares your rootfs for general use and makes sure it can be used for both NFS boot and SD card boot on the Kria platform.
2.5. Configure Timezone and Locales
Optionally, set the timezone to your location. I set it to Europe/Ljubljana
:
ln -sf /usr/share/zoneinfo/Europe/Ljubljana /etc/localtime
2.6. Configure SSH and System Locales
Allow root login via SSH by editing /etc/ssh/sshd_config:
vi /etc/ssh/sshd_config
PermitRootLogin yes
Before enabling the SSH service, it’s important to properly configure the system locales—otherwise, the setup may fail.
sudo dpkg-reconfigure locales
When the menu appears, ensure that en_US.UTF-8 is selected (marked with *), and choose it as the default locale when prompted.
To verify the available locales:.
locale -a
Then make the chosen locale permanent:
update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
Enable SSH service:
systemctl enable ssh
Set passwords for root and create a new user:
passwd root
adduser kria
usermod -aG sudo kria
2.7. Configure Network and DNS
Enable systemd-networkd and systemd-resolved:
systemctl enable systemd-networkd
systemctl enable systemd-resolved
Create a network configuration file at /etc/systemd/network/eth1.network
with the following content:
[Match]
Name=eth1
[Network]
DHCP=yes
At the end, set up DNS resolution again:
ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
ls -l /etc/resolv.conf
At this point, we’re reconfiguring DNS resolution for when the system boots on the actual Kria hardware.
Earlier, we temporarily set /etc/resolv.conf
to a static DNS (e.g., 8.8.8.8) to make sure apt and networking worked correctly inside the chroot environment on the host PC. But on Kria, we want to let systemd-resolved manage DNS dynamically, especially if using DHCP.
This symbolic link ensures that DNS configuration is maintained automatically by the system at runtime — avoiding conflicts or failures once the board is booted with the new rootfs.
It may feel redundant, but it’s necessary to transition from a temporary chroot-friendly setup to a proper system-managed configuration.
Note: Reconfigure DNS both before entering and after exiting the chroot environment each time.
2.8. Exit Chroot and Unmount Filesystems
Once the rootfs is fully configured and all desired packages are installed, exit the chroot environment:
exit
And unmount the filesystems:
umount /mnt/rootfs/dev/pts
umount /mnt/rootfs/dev
umount /mnt/rootfs/proc
umount /mnt/rootfs/sys
Alternatively, you can use the provided umount-chroot-ro.sh
script from the Attachments section, which is especially useful if you plan to repeat the chroot process multiple times.
./umount-chroot-ro.sh
If you later decide to add more packages or tools to your Ubuntu rootfs, simply reuse the provided helper scripts - all inside the docker container.
- Run
mount-chroot-ro.sh
to remount the necessary filesystems, configure DNS and prepare the environment. - Chroot into the rootfs using
chroot /mnt/rootfs /bin/bash.
- Install the desired packages with
apt update
andapt install <package-name>.
- Exit the chroot.
- Run
umount-chroot-ro.sh
to cleanly unmount everything and reconfigure DNS.
This approach saves time and ensures consistency if you’re updating the rootfs incrementally.
NOTE:
The packages installed so far are only the minimum requirements to boot Ubuntu on the Kria board and access it via SSH.
This setup provides a lightweight, functional environment, but it does not include Kria-specific tools such as xmutil, or support for running official applications, loading FPGA bitstreams, or executing AI demos in Docker containers.
If your goal is to run Kria’s official accelerated applications — like the Smartcam demo — you should from this step follow the official Xilinx Kria Ubuntu documentation to install the full Kria runtime environment. That includes upgrading the image, enabling hardware acceleration, and configuring the board for docker-based workflows.
This project focuses on building and booting a working Ubuntu image from scratch; Kria-specific features require additional setup beyond what’s included here.
(Optional) Clean Up or Switch Docker BuilderAfter building your ARM64 rootfs and Docker image, you might want to switch back to your default Docker builder or clean up your environment. Below are some useful commands:
# Switch back to the default Docker builder
docker buildx use default
# List all available Docker builders (the current one is marked with *)
docker buildx ls
# Switch back to your custom builder (e.g., for rebuilding later)
docker buildx use kria-builder
# Delete the custom builder if it's no longer needed
docker buildx rm kria-builder
# Optionally, remove QEMU static library registration
docker run --rm --privileged multiarch/qemu-user-static --reset
These steps help ensure your Docker environment remains clean and prevent potential conflicts when switching between different architectures or projects.
Part 3: Setting Up NFS Server on Host PCInstall the NFS server package on your host machine (not inside the Docker container) if it hasn’t been set up yet. For detailed instructions, refer to the earlier tutorial section:
3.1. Install NFS Server
To install it, run:
sudo apt update
sudo apt install nfs-kernel-server
3.2. Configure NFS Exports
Edit the /etc/exports
file to include the rootfs directory:
/path/to/your/mnt/rootfs *(rw,no_root_squash,async,no_subtree_check)
Apply the export settings:
sudo exportfs -a
Restart the NFS server:
sudo systemctl restart nfs-kernel-server
Part 4: Configuring Kria for NFS BootTo boot Ubuntu over NFS, we need to configure the Kria SOM’s U-Boot environment to load the Linux kernel via TFTP and mount the Ubuntu root filesystem via NFS. This process begins with powering on the Kria board and interrupting the automatic boot sequence to enter the U-Boot prompt.
Important:
The Kria board always boots initially from QSPI flash, where the BOOT.BIN file resides. This BOOT.BIN contains the first-stage bootloader and U-Boot. When the board starts, U-Boot executes and typically continues to boot Linux. However, by pressing any key at the right moment during startup (usually within 2–3 seconds), you can interrupt this process and access the U-Boot prompt.
Once inside the U-Boot shell, we can manually set the necessary environment variables to load the Image
and system.dtb
files from a TFTP server and point the rootfs to our NFS export. This allows the board to boot completely into our custom Ubuntu root filesystem hosted on the development PC, without needing to write anything to the Kria’s local storage.
4.1. Set U-Boot Environment Variables
At the U-Boot prompt on the Kria device, set the necessary environment variables:
setenv serverip 192.168.1.101
setenv ipaddr 192.168.1.27
setenv kernel_addr 0x2000000
setenv fdt_addr 0x1000000
4.2. Load Kernel and Device Tree via TFTP
Make sure that the TFTP server on your host machine is up and running and share both needed kernel files.
Then run these commands in U-Boot.
tftpboot $fdt_addr system.dtb
tftpboot $kernel_addr Image
4.3. Set Boot Arguments and Boot
Configure the bootargs environment variable to point to your NFS-exported root filesystem. Make sure to update your host PC IP address and the path to your rootfs accordingly.
setenv bootargs 'console=ttyPS1,115200 root=/dev/nfs rw nfsroot=192.168.1.100:/path/to/your/mnt/rootfs,v3 ip=dhcp init=/lib/systemd/systemd'
NOTE:
The init=
parameter is especially important here — it explicitly tells the Linux kernel which program to run as PID 1, the first user-space process.
Here, we point it to /usr/lib/systemd/systemd
, which is the default system manager for Ubuntu and many modern Linux distributions. Without this, the system will fail to bring up essential services, leading to a stalled or broken boot process.
4.4 Boot the Linux Kernel
booti $kernel_addr - $fdt_addr
If everything was set up correctly, your Kria board should now boot into the Linux system using the rootfs served via NFS from your host machine. Log in using the credentials you created earlier during the chroot configuration.
To verify network connectivity, run:
ping google.com
If the command fails, it’s likely that DNS was not configured properly before exiting the chroot environment. Revisit the relevant step in this tutorial and ensure that /etc/resolv.conf
is correctly symlinked to enable name resolution.
If you prefer to chroot directly on the Kria device (much slower) using a PetaLinux image:
5.1. Boot Kria with PetaLinux Image
Ensure that the Kria device is booted with a PetaLinux image from Project-2, that includes the necessary tools and services.
5.2. Mount External Rootfs
Assuming the external rootfs is available on an NFS on a host PC, mount it to a directory:
sudo mkdir /mnt/ubuntu-rootfs
sudo mount -o nolock <host-ip-address>:/path/to/nfsroot_ubuntu /mnt/ubuntu-rootfs
5.3. Mount Pseudo Filesystems
sudo mount --bind /dev /mnt/ubuntu-rootfs/dev
sudo mount --bind /proc /mnt/ubuntu-rootfs/proc
sudo mount --bind /sys /mnt/ubuntu-rootfs/sys
sudo mount --bind /run /mnt/ubuntu-rootfs/run
5.4. Chroot into the Mounted Rootfs
chroot /mnt/ubuntu-rootfs /bin/bash
You can now perform configurations directly on the Kria device and install all minimum needed packages.
Part 6: Create Bootable SD Card Image with Custom Ubuntu Rootfs, Kernel & DTB6.1. Overview
In this section, you’ll create a bootable SD card image that contains:
- The Ubuntu root filesystem (built in this tutorial in Part 2)
- Linux kernel
Image
andsystem.dtb
from Project 2 (PetaLinux build from custom .xsa) - A
boot.scr
script to configure boot behavior
Note: This image is not standalone in terms of U-Boot – the Kria SoM boots U-Boot from its internal QSPI flash, but will use the SD card (appears as a USB device to Kria) for loading the kernel, DTB, and rootfs.
6.2. Create Image Directory Layout
First, create a directory structure for the SD card:
mkdir -p sdcard/boot
sdcard/boot
→ forImage
,system.dtb
,boot.scr
sdcard/rootfs
→ mount point for the Ubuntu rootfs
Copy the boot files provided in previous tutorial:
cp /path/to/Image sdcard/boot/
cp /path/to/system.dtb sdcard/boot/
Make a symbolic link that points to the provided rootfs.
ln -s $(pwd)/mnt/rootfs/ ./sdcard/
To check everything:
ls sdcard/boot
ls sdcard/rootfs
The output should be like this:
$ ls sdcard/boot/
Image system.dtb
$ ls sdcard/rootfs
bin boot dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
$
6.3. Create boot.scr Script
Create a boot.cmd
file with the following U-Boot commands or download the file from the Attachment section:
setenv kernel_addr 0x2000000
setenv fdt_addr 0x1000000
echo "Loading device tree..."
fatload usb 0:1 ${fdt_addr} system.dtb
echo "Loading kernel Image..."
fatload usb 0:1 ${kernel_addr} Image
echo "Setting bootargs..."
setenv bootargs 'console=ttyPS1,115200 root=/dev/sda2 rw rootwait earlycon ip=dhcp'
echo "Booting..."
booti $kernel_addr - $fdt_addr
Compile the boot.cmd
script to generate the boot.scr
file, which will be placed in the ./sdcard/boot/
directory.
mkimage -C none -A arm -T script -d ./boot.cmd sdcard/boot/boot.scr
6.4. Create a .wic Image
Place the make_wic_image.sh
script from the Attachment section to the sdcard/
directory and set the file mode to be executable:
cp path/to/make_wic_image.sh sdcard/
cd sdcard/
chmod +x make_wic_image.sh
Now, build the image:
sudo ./make_wic_image.sh
The make_wic_image.sh
script automates the creation of a bootable .wic
image for the Kria board.
It first creates a blank image file and partitions it into two sections: a FAT32 boot partition and an ext4 root filesystem partition.
The script formats each partition appropriately, mounts them, and copies the necessary boot files (like Image
, system.dtb
, and boot.scr
) into the boot partition. It then copies the full Ubuntu root filesystem into the ext4 partition.
This results in a ready-to-use .wic
image that can be flashed to an SD card and used to boot the Kria board directly.
Optionally, compress the image file (done already in the provided script):
zip custom-linux-image.wic.zip ./custom-linux-image.wic
6.5. Inspect the .wic Image Locally - Optionally, for debugging purposes
First, list partitions in the image file:
unzip custom-linux-image.wic.zip
fdisk -l custom-linux-image.wic
It should output something like this:
Units: sectors of 1 * 512 = 512 bytes
..
Device Boot Start End Sectors Size Id Type
custom-linux-image.wic1 * 2048 262143 260096 127M c W95 FAT32 (LBA)
custom-linux-image.wic2 262144 8781823 8519680 4.1G 83 Linux
Then in a Python shell (or similar calculator) calculate the partition offsets by multiplying both start parameters with the block size (512):
>>> 2048 * 512
1048576
>>> 262144 * 512
134217728
>>>
Then mount each partition, only one at a time would work, to explore the content:
mkdir -p ./mnt/wic
# explore fat32 partition
sudo mount -o loop,offset=1048576 custom-linux-image.wic ./mnt/wic
ls ./mnt/wic
sudo umount ./mnt/wic
# explore ext4 partition
sudo mount -o loop,offset=134217728 custom-linux-image.wic ./mnt/wic
ls ./mnt/wic
sudo umount ./mnt/wic
You should see the same files you previously added to the image. This process is helpful for debugging or verifying the image contents if you’re unsure about its current state later on.
6.6. Boot Kria board from SD card
To boot the Kria board from the SD card, first flash the generated .wic image to an SD card using a tool like Balena Etcher or similar. Once flashed, insert the SD card into the SD card slot on the Kria board.
Power on the board — U-Boot will launch from the QSPI flash (which contains the pre-installed BOOT.BIN
) and execute the boot.scr
script located on the SD card.
If everything is set up correctly, the system will boot into your custom Ubuntu root filesystem. Log in using the username and password you set up during chroot, and verify internet connectivity by running:
ping google.com
If the ping fails, it’s likely due to misconfigured DNS — refer back to the DNS configuration section to correct the issue.
6.7. Final Notes on Kria Boot Behavior
Kria boots U-Boot from QSPI flash: This flash contains a BOOT.BIN
with FSBL, PMUFW, and U-Boot.
NOTE: SD card appears as USB device: When inserted into the Kria carrier board’s slot, it shows up to U-Boot as usb 0:1 – not mmc 0.
U-Boot runs our boot.scr
: It loads the kernel and DTB from usb 0:1 and boots with rootfs on SD card.
We have built a complete, working Ubuntu system for the Kria KV260 platform:
- The rootfs is based on official Ubuntu Base ARM64, customized with necessary packages and network/SSH setup.
- The kernel and DTB come from a PetaLinux build using a custom XSA (see Project-2).
- The system is configured for NFS boot, using a host PC as the NFS server.
- We’ve created a bootable SD card image that U-Boot uses to load the kernel and DTB.
This setup provides a powerful and flexible embedded Linux environment, combining the robustness of PetaLinux boot with the user-friendliness of Ubuntu.
Next StepsIn the next tutorial, you’ll learn how to:
Automate the creation of a custom Ubuntu-based SD card image for the AMD/Xilinx Kria KV260 board using Docker.
Comments