Building a custom Ubuntu kernel
Most of the time the stock Ubuntu kernel is exactly what you want and you should leave it alone. Occasionally you need to change something the packaged kernel will not let you: patch an in-tree driver, flip a build-time option, or tweak the input subsystem. In my case it was the input drivers, trimming how evdev, mousedev and joydev behave so a touchscreen-only device did not expose a stray pointer. Whatever the reason, the clean way on Ubuntu is to build the kernel into .deb packages and install those, so it is tracked by the package manager like any other kernel.
This is on Ubuntu 22.04 building a 5.15 series kernel.
Build dependencies
sudo apt install build-essential libncurses-dev flex bison openssl libssl-dev \
dkms libelf-dev libudev-dev libpci-dev libiberty-dev autoconf bc zstd dwarves rsync
libelf-dev, bc, dwarves and zstd are the ones people miss, and the build fails partway through without them.
Get the source
Pull a mainline tarball from kernel.org and unpack it into /usr/src:
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.46.tar.xz
tar -xf linux-5.15.46.tar.xz
sudo mv linux-5.15.46 /usr/src/
cd /usr/src/linux-5.15.46
Start from your running config
Do not configure from scratch. Start from the config of the kernel you are already running, so you keep all the driver and hardware choices that make your machine boot, then change only what you came to change:
cp /boot/config-$(uname -r) .config
Carry forward any new options non-interactively, accepting the defaults:
make olddefconfig
If you need to make your changes through the menu interface, do it now:
make menuconfig
Or, if you are patching driver source directly, edit the files under drivers/ before you build.
Module signing certificate
Modern kernel configs sign their modules, and the build will stop asking for a signing key if there is not one. Generate a self-signed certificate for it:
mkdir -p certs
openssl req -x509 -newkey rsa:4096 -keyout certs/mycert.pem -out certs/mycert.pem -nodes -days 3650
If you would rather not sign modules at all for a local build, turn CONFIG_MODULE_SIG off in the config instead; signing is the better habit.
Build the packages
Build straight into Debian packages, using all your cores. LOCALVERSION tags the kernel so it is clearly yours and does not collide with the stock one in the boot menu:
make -j$(nproc) bindeb-pkg LOCALVERSION=-custom
This takes a while. When it finishes, the .deb files are written to the parent directory, /usr/src here.
Install and boot
Install the image and headers packages and reboot:
cd /usr/src
sudo dpkg -i linux-image-*-custom_*.deb linux-headers-*-custom_*.deb
sudo reboot
Confirm you are running the kernel you built:
uname -r
You should see your -custom version. Because these went in as proper packages, the kernel shows up in the boot menu, is managed by apt, and removes cleanly with dpkg -r if the experiment does not work out, which is the whole reason to build it this way rather than running make install by hand.