Skip to content

Custom image using mkosi

Note

The following instruction is for building a Debian bookworm image. When building other distributions, the instruction will not work without modifications.

Note

The build was done on a WSL2 installation of Ubuntu 24.04.

mkosi

A re-introduction to mkosi -- A Tool for Generating OS Images

Install

Note

The guide was developed when running as root

Follow the instructions at Installation to install mkosi.

This guide was tested using the following method:

apt install -y pipx git
pipx install git+https://github.com/systemd/mkosi.git
export PATH=$PATH:/root/.local/bin

For cross-build support of arm64 on x86-64 architecture and support for Debian repositories the following packages must be installed

apt install -y qemu-user-static debian-archive-keyring

For testing using systemd-nspawn:

apt install -y systemd-container

Documentation

For further instructions, see man mkosi

Configuration

Create the following files and directory structures in the same directory path.

The configuration below is for building Debian Bookworm for arm64 and generating an rootfs.tar.xz output that can be used to create an LXC container.

The package list can be modified to include the packages needed or to e.g. change the editor from nano to vim. The only strictly necessary package is init, without it the container will not run on the Edge device.

mkosi.conf

[Distribution]
Distribution=debian
Release=bookworm
Architecture=arm64

[Output]
Output=rootfs
Format=tar
CompressOutput=xz

[Content]
# Cleanup temporary shadow paths
RemoveFiles=/etc/*-
Packages=
    apt
    init
    systemd-resolved
    dbus
    iproute2
    iputils-ping
    netbase
    net-tools
    openssh-client
    procps
    ca-certificates
    nano

BuildPackages=gcc,libc6-dev

The BuildPackages are only used when building the test application and will not be included in the final image.

The following is a post-installation script that runs inside the chroot environment during the image build process. It enables custom modifications to the final root filesystem. To ensure it can be executed, set the appropriate permissions with chmod +x mkosi.postinst.chroot.

mkosi.postinst.chroot

#!/bin/bash
set -eux

# The firstboot services hangs, disable for now
systemctl mask systemd-firstboot.service

# LXC template support for hostname
echo "LXC_NAME" > /etc/hostname
sed -i '1i127.0.1.1       LXC_NAME' /etc/hosts

The following file is used to setup the root password, the defined content will result in a passwordless root login. Set the file permissions so that only the owner can access it, using chmod 600 mkosi.rootpw.

mkosi.rootpw

hashed:

Files in mkosi.extra directory are copied into the image after distribution is installed.

Configuration for systemd management of eth0 and enable DHCP client.

mkosi.extra/etc/systemd/network/eth0.network

[Match]
Name=eth0

[Network]
DHCP=true

[DHCPv4]
UseDomains=true

[DHCP]
ClientIdentifier=mac

Add Debian sources for apt.

mkosi.extra/etc/apt/sources.list

deb http://deb.debian.org/debian bookworm main contribdeb
deb http://deb.debian.org/debian bookworm-updates main contribdeb
deb http://deb.debian.org/debian-security/ bookworm-security main contrib

Host journal

Note

This is an Edge specific configuration and is not meant for general mkosi configuration.

This is an example of how to automatically symlink the host systemd journal with the containers journal, as described here, using systemd.

mkosi.extra/usr/bin/symlink-host-journal

#!/bin/sh -eu

JOURNAL_PATH=/var/log/journal

for file in "$JOURNAL_PATH"/*; do
    if [ -L "$file" ]; then
        symlink="$file"
        target=$(realpath "$symlink")
        if [ ! -e "$target" ]; then
            echo "Remove symlink: $symlink"
            rm -rf "$symlink"
        fi
    fi
done

for file in /host/journal/*; do
    base=$(basename "$file")
    target="$JOURNAL_PATH/$base"
    if [ ! -e "$target" ]; then
        ln -s "$file" "$target"
        echo "Created symlink $target -> $file"
    fi
done

This file is the script that is run at startup, it removes the old symlink if it no longer exists and adds the new one if it does not exist. Set the file permissions so that it can be executed, chmod 750 mkosi.extra/usr/bin/symlink-host-journal.

Note

Depending on how the systemd-journal is configured the path to the journal can change. If it is configured to be persistent /var/log/journal is used and /run/log/journal when it is configured as volatile. Change the JOURNAL_PATH varable according to the systemd-journal configuration.

To add the script as a service that is run on startup of the container the following configuration needs to be added.

mkosi.extra/etc/systemd/system/symlink-host-journal.service

[Unit]
Description=Symlink the host journal to the container journal directory

[Service]
Type=oneshot
ExecStart=/usr/bin/symlink-host-journal

[Install]
WantedBy=default.target

Build the image

Run commands from the same directory as the configuration files.

mkosi

The result of the build operation is a rootfs.tar.xz file that will work with the LXC image meta.tar.xz.

Test the image

The image can be tested using lxc:

lxc-create hostname -t local -- --fstree rootfs.tar.xz --meta meta.tar.xz
lxc-start hostname
lxc-attach hostname

The image can be also be tested using systemd-nspawn:

# Unpack to directory
mkdir -p image && tar xJf rootfs.tar.xz -C image

# Start using systemd init
systemd-nspawn -D image --resolv-conf=bind-host --register=no --keep-unit --boot

# or just enter a shell
systemd-nspawn -D image --resolv-conf=bind-host --register=no --keep-unit

--register=no and --keep-unit is currently needed to be able to run the systemd-nspawn command multiple times.

During development iterations the tar steps can be skipped.

# Build into directory
mkosi --format=directory

systemd-nspawn -D rootfs --resolv-conf=bind-host --register=no --keep-unit --boot

Warning

The testing might give unexpected results when relying on the qemu emulation support. The recommendation is to do the testing in a native environment.

Higher efficiency and better development host compatibility can be achieved with the native build.

mkosi --format=directory --architecture x86-64

# The container will now run without emulation
systemd-nspawn -D rootfs --resolv-conf=bind-host --register=no --keep-unit --boot

Building custom application

This section will show a simple example of how to include as custom built application into the image rootfs.

Preparation

Cache

Setup build cache directories

mkdir mkosi.cache mkosi.builddir

Source

hello.c

#include <stdio.h>

int main() {
    printf("Hello, world!\n");
    return 0;
}

Build and install script

mkosi.build.chroot

#!/bin/bash

set -eux

# Build test application
cd $BUILDDIR
gcc $SRCDIR/hello.c -o hello

# Install test application into /root
mkdir -p $DESTDIR/root/
cp hello $DESTDIR/root/

Build and test

The incremental build is enabled to take advantage of caching and the force flag to make it possible to rebuild even if a previous image already exists. If possible, build and test native first for better efficiency.

mkosi --format=directory --incremental yes --architecture x86-64 --force

# The container run without emulation
systemd-nspawn -D rootfs --resolv-conf=bind-host --register=no --keep-unit --boot

The final build is a cross build resulting in a LXC compatible rootfs.

mkosi