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
sudoprivileges. - 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-devprovides the text interface ofmake menuconfig.libssl-devandlibelf-devare needed for module signing and theobjtoolutility.dwarvesprovidespahole, required to generate BTF type information, which eBPF tools use.dpkg-dev,debhelperandfakerootbuild the.debpackages.
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:
gpg --locate-keys [email protected] [email protected]
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
Tip
make localmodconfigdisables every module that is not loaded right now, which can cut build time in half. Only use it if you are sure all the hardware and features you need (storage, network, filesystems, firewall modules) are in use at that moment, or the new kernel may miss a driver it needs to boot.
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.
