I find myself fairly regularly hearing about new features in the Linux kernel (the core of Linux operating systems), getting excited, and checking to see if my distribution’s repository of packages has the latest kernel built. Unfortunately, Ubuntu’s repositories don’t get new kernel versions for a long time. Ubuntu 24.04 LTS still ships a 6.8 kernel, while kernel.org is on 7.2 as I’m writing this.
Fortunately, the Linux kernel is open source, so we can freely download the kernel’s source code, compile it, and install it.
This article walks through building the current stable kernel on Ubuntu 24.04 LTS (Noble Numbat) and installing it as a normal .deb package. The same steps work on Debian and most other Ubuntu releases. (I originally wrote this for Ubuntu 19.10 and Linux 5.4 back in 2020, and updated it in 2026, since several steps no longer work the old way.)
TL;DR
sudo apt install -y git build-essential bc bison flex libncurses-dev libssl-dev \
libelf-dev libdw-dev dwarves debhelper rsync cpio kmod python3 ccache
git clone --depth 1 -b linux-7.2.y https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
cd linux
cp /boot/config-$(uname -r) .config
scripts/config --set-str SYSTEM_TRUSTED_KEYS "" --set-str SYSTEM_REVOCATION_KEYS ""
make olddefconfig
make -j"$(nproc)" bindeb-pkg LOCALVERSION=-custom
sudo dpkg -i ../linux-image-*-custom_*.deb ../linux-headers-*-custom_*.deb
sudo reboot
Read on for what each step does, how to make the build a lot faster, and how to get back to your old kernel if the new one doesn’t boot.
Before you start: do you actually need to compile it?
If you just want a newer kernel and don’t care about changing its configuration, Ubuntu publishes prebuilt mainline kernels at kernel.ubuntu.com/mainline. That’s honestly what I run day to day on my desktop. They’re unsupported test builds, though, and they aren’t signed for Secure Boot. Building it yourself is how you get to change options, apply patches, or just learn how your system works, which is the fun part.
Installing the build tools
Although we can freely download the code and compile it, we still need some applications installed to download the code and compile the kernel. We’ll need:
git, to download the kernel’s source codebuild-essential, which gives us the compiler (gcc) andmakebc,bison, andflex, which the kernel’s build scripts use to generate codelibncurses-dev, so we can use the menu-basednconfigto configure the kernellibssl-dev, which the kernel uses to sign its moduleslibelf-dev,libdw-dev, anddwarves, for the ELF/debugging tools newer kernels need (dwarvesprovidespahole)debhelper,rsync,cpio,kmod, andpython3, which the kernel’s built-in Debian packaging uses to turn the build into.debfilesccache(optional), to cache compiled files, which makes rebuilds way faster
All of these can be installed by running:
sudo apt install -y git build-essential bc bison flex libncurses-dev libssl-dev \
libelf-dev libdw-dev dwarves debhelper rsync cpio kmod python3 ccache
If you’re following an older guide (including the original version of this one), you’ll see kernel-package in the list. That package was removed from Ubuntu years ago and isn’t needed anymore, since the kernel can build its own Debian packages now.
You’ll also want plenty of free disk space. A build with the full Ubuntu configuration can use 20-30 GB, though the faster option further down uses a lot less.
Downloading the source code
The first thing to do is figure out which version to build. The front page of kernel.org lists the current releases:
- mainline is the newest development version (the
-rcreleases). Fun, but not what you want on a machine you rely on. - stable is the newest finished release, getting bug fixes. This is what I’m building here.
- longterm releases get fixes for years, if you’d rather not chase new versions.
Each version lives on its own branch in the stable repository at git.kernel.org. For example, the branch linux-7.2.y holds Linux 7.2 and all of its bug-fix releases (7.2.1, 7.2.2, and so on). In this article, I’m going to build Linux 7.2, so I’ll use that branch. Swap in whatever version is current when you read this.
git clone --depth 1 -b linux-7.2.y https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
cd linux
This tells git to clone, or copy, the code from the linux-7.2.y branch. The --depth 1 part is new since the original version of this post, and it’s a big one: it only downloads the latest code instead of the kernel’s entire history going back to 2005, which turns a multi-gigabyte download into a few hundred megabytes.
If you’d rather skip git entirely, you can also download the tarball link from kernel.org’s front page and extract it with tar xf linux-*.tar.xz.
Configuring the kernel
Now that we have the source code, we need to configure the kernel. There are thousands of options, so we’re not going to pick them one by one. The easiest and safest starting point is the configuration Ubuntu used to build the kernel you’re running right now, which lives in /boot:
cp /boot/config-$(uname -r) .config
uname -r prints your current kernel’s version (I wrote a whole post about uname), so this copies the matching config file into the source directory as .config.
Fixing the Ubuntu certificate error
Ubuntu’s config tells the kernel to build in Canonical’s signing certificates, from files that only exist in Ubuntu’s own source tree. If you skip this step, the build fails partway through with an error like No rule to make target 'debian/canonical-certs.pem'. Blank those two options out:
scripts/config --set-str SYSTEM_TRUSTED_KEYS "" --set-str SYSTEM_REVOCATION_KEYS ""
Updating the config for the new kernel
Our config file came from an older kernel, so it doesn’t have answers for any options that were added since. This fills in the defaults for all of them:
make olddefconfig
In the original version of this post, I used yes '' | make oldconfig, which does the same thing by hitting Enter at every question. olddefconfig is the built-in way to do that.
Optional: making the build a lot faster
Ubuntu’s config builds drivers for basically every piece of hardware that exists, which is why a full build takes so long. If you’re building a kernel just for the machine you’re on, localmodconfig trims the config down to only the modules that are loaded right now:
yes '' | make localmodconfig
This can take a build from hours down to minutes. The catch is that it really does only keep what’s loaded, so plug in everything you normally use (USB drives, webcam, VPN, phone tethering, etc.) before running it, or the new kernel won’t have drivers for them. I’d skip this for a kernel you plan to give to other people.
Ubuntu’s config also builds full debugging information, which makes the build slower and produces a huge extra -dbg package. If you don’t need it, turn it off:
scripts/config --disable DEBUG_INFO_DWARF5 --enable DEBUG_INFO_NONE
make olddefconfig
One thing to know: this also turns off BTF type information, which some eBPF tools (like bpftrace) rely on. If you use those, leave debug info on.
Changing options with nconfig
Normally, no additional configuration is necessary when you’ve started from the old kernel’s config. If you do want to look around or change something, fire up the configuration menu with make nconfig.

make nconfignconfig has every option for your kernel in it. Any option with ---> to the side means that there’s a submenu. Use the arrow keys on your keyboard to move around, and use the Enter key to open a menu or toggle an option. F8 lets you search for an option by name, which is a lot faster than digging through menus.
Many options can be set to one of three states:
- A space means that it’s not selected to be built
*means that it will be built into the kernel itself, and always loadedMmeans that it will be built as a module, and only loaded when needed

Device DriversTo save your configuration, press F6, and F9 to quit.
Building the kernel
I’m going to cover compiling installable packages, so that it’s easy to switch between kernels (and remove them), and so that the kernel can be copied to other machines as .deb files. To start the build, run:
time make -j"$(nproc)" bindeb-pkg LOCALVERSION=-custom
Let’s dig into what this command does.
timeshows how long the build took once it’s donemakeis the build command-j"$(nproc)"tells make to run as many jobs at once as we have processor coresbindeb-pkgsays that we wantmaketo build installable Debian packages. (The olderdeb-pkgtarget also builds a source package, which we don’t need.)LOCALVERSION=-customadds-customto the end of the kernel version, so we can tell which installed kernels are our own builds
Notice there’s no sudo. The original version of this post ran the build as root, which you shouldn’t do: the build doesn’t need it, and the packaging step fakes root permissions on its own. The only step that needs sudo is installing.
If you installed ccache and expect to rebuild a few times (say, while tweaking options), add CC="ccache gcc" to the command. The first build won’t be any faster, but rebuilds will only recompile what changed.
Now we play the waiting game. With the full Ubuntu config, expect anywhere from about half an hour on a fast desktop to several hours on a laptop or VM. (My original build in a 2020 virtual machine took over an hour and a half.) With localmodconfig, it’s often just a few minutes.
Installing the new kernel
The packages end up in the directory above the source code. For example, if your source is in ~/src/linux, the packages are in ~/src. The names will look something like this, with your version number in them:
linux-headers-7.2.8-custom_7.2.8-1_amd64.deb
linux-image-7.2.8-custom_7.2.8-1_amd64.deb
linux-libc-dev_7.2.8-1_amd64.deb
(If you left debug info on, there’ll also be a big linux-image-...-dbg package, which you can ignore.)
Install the image and headers packages:
sudo dpkg -i ../linux-image-*-custom_*.deb ../linux-headers-*-custom_*.deb
The headers package is what lets things like NVIDIA or VirtualBox drivers (through DKMS) build their modules for your new kernel. I’d leave linux-libc-dev alone, though: it replaces the kernel headers your whole system compiles programs against, which you don’t need to do just to run a new kernel.
If you use Secure Boot
Most newer PCs ship with Secure Boot turned on, and a kernel you built yourself isn’t signed by Canonical, so the machine will refuse to boot it (usually with an “invalid signature” or “bad shim signature” message). You can check with mokutil --sb-state. If it’s enabled, you can either turn Secure Boot off in your firmware settings, or sign the kernel with your own key and enroll it with mokutil. Signing is more work, but it’s the way to go if you want to keep Secure Boot on.
Booting the new kernel
Installing the package adds the new kernel to GRUB and makes it the default automatically, since it’s the newest version installed. I like to make GRUB show its menu for a few seconds anyway, so it’s easy to pick the old kernel if something goes wrong. Open the GRUB settings with sudoedit /etc/default/grub and set these two lines:
GRUB_TIMEOUT_STYLE=menu
GRUB_TIMEOUT=10
On Ubuntu, GRUB_TIMEOUT_STYLE is set to hidden by default, which is why you normally never see the menu. To make these changes apply, run sudo update-grub, then restart with sudo reboot.
When the menu shows up, your new kernel will already be selected. If you ever want a different one, choose Advanced options for Ubuntu and you’ll see your new kernel, your old one, and recovery mode options. (If you didn’t change the GRUB settings, you can still get to the menu by holding Shift, or tapping Esc on UEFI machines, while the computer starts.)

Once the machine finishes booting, pop open a terminal and run uname -r. You’ll see that we’re running the kernel we just built:
$ uname -r
7.2.8-custom
If the new kernel doesn’t boot
It’s pretty quick and easy to get back on the old kernel again. If the system is frozen, hold Alt + SysRq (usually the Print Screen key), and while holding those tap S, U, then B. That syncs your disks, remounts them read-only, and reboots, which is a lot safer than holding the power button. (You might have seen the longer R E I S U B version of this. Ubuntu only allows the sync, remount, and reboot keys by default, so S U B is the part that does anything.) If even that doesn’t work, hold the power button.
When GRUB comes up, go to Advanced options for Ubuntu, and select your old kernel (one that doesn’t have -custom at the end). You should be back at a working system.
A failed boot is usually caused by the kernel missing a driver your computer needs to start, most often for your disk or filesystem, which tends to happen after using localmodconfig. Try configuring again without it, or add the missing drivers in make nconfig. If it got far enough to start, journalctl -b -1 on the old kernel shows the logs from the failed boot, which usually point at the problem.
Since we installed the kernel as a package, removing a custom kernel you don’t want anymore is just:
sudo apt remove linux-image-7.2.8-custom linux-headers-7.2.8-custom
Wrapping up
Congratulations on being one of the Linux users who have built the core of their system on their own. Have fun looking into what all of the configuration options are. When a new stable release comes out, the whole process is just: grab the new branch, copy your config over, run make olddefconfig, and build again.
If you’re looking for a pretty cool customization, look into the “realtime” (PREEMPT_RT) options. Realtime support used to be a separate set of patches, but it was merged into the mainline kernel in Linux 6.12, so on a current kernel it’s just an option in make nconfig. It’s hidden until you turn on General setup → Configure standard kernel features (expert users), and then Fully Preemptible Kernel (Real-Time) shows up under General setup → Preemption Model.