Ubuntu ships a well tested kernel, and for most servers it is the right choice. Building your own makes sense when you need a newer upstream release for a driver or feature, want to enable an option that Ubuntu leaves out, or want to test a patch. The risk is real: a kernel that does not boot leaves the server unreachable over SSH, so this tutorial keeps the stock kernel installed as a fallback.

You will download a longterm release from kernel.org, verify its signature, reuse the configuration of the running Ubuntu kernel as a starting point, build it as .deb packages with the kernel's own packaging target, install them and boot into the new kernel. Packaging the kernel means it can be removed cleanly with apt later.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS on x86_64, for example a CubePath VPS.
  • A non-root user with sudo privileges.
  • At least 4 CPU cores and 4 GB of RAM. The build takes roughly 30 to 90 minutes with 4 cores, depending on the configuration.
  • At least 30 GB of free disk space in your home directory, plus about 500 MB in /boot.
  • Access to the server console (the web console of your VPS provider, or IPMI on a dedicated server), so you can pick the old kernel in GRUB if the new one fails to boot.

Check the running kernel and the available space before starting:

uname -r
df -h ~ /boot
6.8.0-85-generic
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        78G  4.1G   70G   6% /
/dev/vda1        78G  4.1G   70G   6% /

If Secure Boot is enabled, the firmware refuses to load a kernel you signed yourself unless you enroll your own key. Virtual servers usually have it disabled. Check it:

mokutil --sb-state
SecureBoot disabled

If the command reports SecureBoot enabled, you need to sign the kernel and enroll a Machine Owner Key, which is outside the scope of this guide.

Step 1 - Installing the build dependencies

The kernel needs a compiler toolchain, parser generators, and development headers for the libraries used by its build tools, plus the Debian packaging tools for the .deb output:

sudo apt update
sudo apt install build-essential bc bison flex libncurses-dev libssl-dev libelf-dev \
  dwarves zstd cpio rsync kmod fakeroot dpkg-dev debhelper gnupg wget

What the less obvious packages are for:

  • libncurses-dev provides the text interface of make menuconfig.
  • libssl-dev and libelf-dev are needed for module signing and the objtool utility.
  • dwarves provides pahole, required to generate BTF type information, which eBPF tools use.
  • dpkg-dev, debhelper and fakeroot build the .deb packages.

Step 2 - Downloading and verifying the kernel source

Open kernel.org and choose a longterm release. Longterm branches receive fixes for several years, which is what you want on a server. Set the version in a shell variable; the examples use 6.12, replace 6.12.50 with the latest 6.12 (or newer longterm) point release listed on the site:

KVER=6.12.50

Create a working directory and download the source and its signature:

mkdir -p ~/kernel && cd ~/kernel
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${KVER}.tar.xz
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${KVER}.tar.sign

Stable and longterm releases are signed by Greg Kroah-Hartman or Linus Torvalds. Import their keys through the kernel.org Web Key Directory:

The signature covers the uncompressed tarball, so decompress it on the fly while verifying:

xz -cd linux-${KVER}.tar.xz | gpg --verify linux-${KVER}.tar.sign -
gpg: Signature made ...
gpg:                using RSA key 647F28654894E3BD457199BE38DBBDC86092693E
gpg: Good signature from "Greg Kroah-Hartman <[email protected]>" [unknown]
gpg: WARNING: This key is not certified with a trusted signature!

Good signature is the line that matters. The warning only means you have not personally certified the key. If you see BAD signature, delete the files and download them again.

Extract the source and enter the directory:

tar -xf linux-${KVER}.tar.xz
cd linux-${KVER}

Step 3 - Creating the kernel configuration

Writing a configuration from scratch means answering thousands of questions. The practical starting point is the configuration of the running Ubuntu kernel, which is known to work on your hardware:

cp /boot/config-$(uname -r) .config

Update it for the new version. olddefconfig keeps every existing answer and uses the default for options that did not exist in the older kernel:

make olddefconfig

Ubuntu's configuration refers to Canonical's certificate files, which do not exist in the upstream source, and the build fails later if you keep them. Clear both settings and set a suffix that identifies your build in uname -r:

scripts/config --set-str SYSTEM_TRUSTED_KEYS ""
scripts/config --set-str SYSTEM_REVOCATION_KEYS ""
scripts/config --set-str LOCALVERSION "-custom"

Making your own changes

Now change whatever made you build a kernel in the first place. Use the menu interface to browse and search options (press / to search, ? for help on an option):

make menuconfig

Or set specific options from the command line, which is easier to repeat. For example, to build the WireGuard module and use a 1000 Hz timer:

scripts/config --module WIREGUARD
scripts/config --disable HZ_250 --enable HZ_1000

Option names are the ones shown by ? in menuconfig, without the CONFIG_ prefix. Resolve dependencies again after any manual change:

make olddefconfig

Verify that your changes survived; an option whose dependencies are not met is silently reset:

grep -E '^CONFIG_(LOCALVERSION|WIREGUARD|HZ)=' .config
CONFIG_LOCALVERSION="-custom"
CONFIG_HZ=1000
CONFIG_WIREGUARD=m

Step 4 - Building the kernel packages

The bindeb-pkg target compiles the kernel and modules and packages them as .deb files in the parent directory. Use all CPU cores with -j$(nproc):

make -j"$(nproc)" bindeb-pkg

The build prints thousands of lines and takes a while. If you are connected over SSH, run it inside tmux or screen so a dropped connection does not stop it. When it finishes without an error, list the packages:

ls ../*.deb
../linux-headers-6.12.50-custom_6.12.50-1_amd64.deb
../linux-image-6.12.50-custom_6.12.50-1_amd64.deb
../linux-image-6.12.50-custom-dbg_6.12.50-1_amd64.deb
../linux-libc-dev_6.12.50-1_amd64.deb

The exact names depend on the version and configuration. You need the linux-image package; the linux-headers package is only needed to build external modules (for example with DKMS). Do not install linux-libc-dev: it would replace the userspace headers provided by Ubuntu.

Step 5 - Installing the kernel

Install the image and headers packages. The patterns below match the files from Step 4 without picking up the -dbg package:

sudo apt install ../linux-image-${KVER}-custom_*.deb ../linux-headers-${KVER}-custom_*.deb

The package scripts copy the kernel to /boot, generate an initramfs with update-initramfs and regenerate the GRUB menu. Check that all three pieces are in place:

ls /boot | grep custom
System.map-6.12.50-custom
config-6.12.50-custom
initrd.img-6.12.50-custom
vmlinuz-6.12.50-custom

GRUB boots the highest kernel version by default, so the new kernel will be selected on the next boot. The stock Ubuntu kernel remains in the Advanced options for Ubuntu submenu.

To make sure you see the GRUB menu on the console if something goes wrong, show it for a few seconds instead of hiding it:

sudo nano /etc/default/grub

Set these two values:

GRUB_TIMEOUT_STYLE=menu
GRUB_TIMEOUT=5

Then regenerate the GRUB configuration:

sudo update-grub

Step 6 - Booting and verifying the new kernel

Open the server console in another window, then reboot:

sudo reboot

Once the server is back, log in and check the running version:

uname -r
6.12.50-custom

Look for errors from the boot and confirm that no service failed:

sudo dmesg --level=err,warn | head -n 20
systemctl --failed
  UNIT LOAD ACTIVE SUB DESCRIPTION

0 loaded units listed.

Verify the options you changed in Step 3 are active, for example:

grep CONFIG_HZ= /boot/config-$(uname -r)
sudo modprobe wireguard && lsmod | grep wireguard

Finally, check networking, disks and the application the server runs before you consider the kernel production ready.

Step 7 - Removing or updating the custom kernel

If you want to go back to the Ubuntu kernel, boot it from the Advanced options menu and then remove the custom packages:

sudo apt remove linux-image-6.12.50-custom linux-headers-6.12.50-custom

Unlike Ubuntu's kernels, your build does not receive security updates. When a new point release of your longterm branch appears, repeat Steps 2 to 6 with the new version, copying the .config from the previous build directory instead of /boot/config-*, and remove the previous custom kernel after the new one boots correctly. Keep the stock Ubuntu kernel packages installed and updated as your fallback.

Troubleshooting

No rule to make target 'debian/canonical-certs.pem'. The certificate settings from Ubuntu's configuration are still set. Run the two scripts/config --set-str ... "" commands from Step 3, then make olddefconfig, and start the build again.

BTF: .tmp_vmlinux.btf: pahole (pahole) is not available. Install dwarves and rebuild.

The build stops with Killed or ld terminated with signal 9. The server ran out of memory during linking. Lower the number of parallel jobs, for example make -j2 bindeb-pkg, or add swap.

The new kernel panics with VFS: Unable to mount root fs. The kernel cannot find the driver for the root disk, usually because localmodconfig removed it or the initramfs is missing. Boot the Ubuntu kernel from GRUB, check that /boot/initrd.img-<version>-custom exists, and make sure the storage driver (for example CONFIG_VIRTIO_BLK on a KVM virtual machine) is enabled.

The server does not come back after the reboot. Use the console to pick the Ubuntu kernel under Advanced options for Ubuntu, then inspect the previous boot with journalctl -b -1 -k.

Conclusion

You have downloaded and verified a longterm kernel from kernel.org, configured it from your working Ubuntu configuration, built it as .deb packages, installed it alongside the stock kernel and booted into it. Because it is packaged, removing or replacing it is a single apt command.

As next steps, keep your .config under version control so every rebuild is reproducible, subscribe to the stable release announcements to know when to rebuild, and benchmark your workload on both kernels to confirm the change was worth it.