I built my first custom kernel out of stubbornness more than necessity — I wanted to strip out every driver I didn’t need on an old laptop and see how much faster it would boot. It taught me more about how Linux actually works than any amount of reading ever had. Kernel configuration looks intimidating from the outside, thousands of options in a maze of menus, but once you understand the structure behind it, it becomes a genuinely enjoyable part of systems work. Here’s everything I’ve learned about doing it properly.
Why You’d Configure a Kernel At All
Most people never touch this because distribution kernels are built to be broadly compatible across huge ranges of hardware. But there are real reasons to configure your own:
- Performance tuning for a specific, known hardware target, stripping unnecessary drivers and enabling architecture-specific optimizations.
- Security hardening, disabling unused subsystems and attack surface, enabling hardening features not on by default.
- Embedded and minimal systems, where kernel size and memory footprint matter enormously.
- Enabling experimental or out-of-tree features not present in your distribution’s stock kernel.
- Debugging and kernel development, where you need debug symbols, tracing infrastructure, or specific subsystem instrumentation.
- Learning, honestly — there’s no better way to understand how Linux is organized than to page through
make menuconfigfor an evening.
Getting the Kernel Source
Before configuring anything, you need source. I usually either grab my distribution’s source package or pull straight from kernel.org for a vanilla build:
# Debian/Ubuntu: get the source matching the running kernel
apt-get source linux-image-$(uname -r)
# Or download a vanilla kernel tarball directly
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.9.tar.xz
tar -xJf linux-6.9.tar.xz
cd linux-6.9
The Configuration File: .config
Everything about kernel configuration revolves around a single text file at the root of the kernel source tree: .config. It’s a flat list of CONFIG_* variable assignments, one per kernel feature or driver, like:
CONFIG_SMP=y
CONFIG_EXT4_FS=y
CONFIG_USB_SUPPORT=y
# CONFIG_DEBUG_KERNEL is not set
CONFIG_HZ=250
Each line is either y (built directly into the kernel image), m (built as a loadable module), a number/string value, or commented out entirely (meaning disabled). Understanding this format matters because at some point you’ll want to grep it, diff it, or script against it directly.
Starting Point: Don’t Start From Scratch
I never start a .config from nothing. That’s a recipe for a kernel that won’t even boot because you missed something critical like your root filesystem driver or your disk controller. Instead, I start from a known-good baseline:
# Use the currently running kernel's config as a starting point
zcat /proc/config.gz > .config # if your running kernel exposes this
# or, commonly available on Debian/Ubuntu:
cp /boot/config-$(uname -r) .config
If /proc/config.gz isn’t available, check if CONFIG_IKCONFIG_PROC was enabled in your running kernel; if not, the copy from /boot/config-$(uname -r) almost always works since distributions ship this file alongside the kernel image.
Configuration Interfaces
The kernel build system (Kconfig/Kbuild) gives you several front-ends to edit .config, all of which read and write the same underlying format.
make menuconfig
This is the one I reach for most. It’s an ncurses-based menu interface, navigable entirely from the terminal:
make menuconfig
This requires libncurses-dev (Debian/Ubuntu) or ncurses-devel (RHEL/Fedora) installed. Navigate with arrow keys, Enter to descend into a submenu, Space to toggle a feature between built-in, module, and disabled, / to search for a specific config symbol, and Esc Esc to back out of a menu.
make xconfig / make gconfig
Graphical Qt-based (xconfig) or GTK-based (gconfig) front-ends, useful if you’re working on a desktop with X11 or Wayland available and prefer mouse navigation over the terminal UI. I use these rarely, mostly when demonstrating kernel config to someone new who finds ncurses menus disorienting.
make xconfig
make config
The oldest, most tedious interface: it walks through every single option sequentially, prompting you one at a time in the terminal. I’ve used it exactly once, out of curiosity, and would not recommend it for real work — there are thousands of prompts.
make config
make oldconfig
This one is essential when you’ve copied an existing .config from an older kernel version into a newer source tree. It walks you through only the new options that didn’t exist in your old config, keeping everything else as-is:
make oldconfig
make olddefconfig
Similar to oldconfig, but instead of prompting you interactively for new options, it silently accepts the kernel’s default answer for anything new. This is what I use in automated build pipelines where no human is present to answer prompts:
make olddefconfig
make defconfig
Generates a distribution-provided or architecture-default configuration from scratch, ignoring any existing .config. Good for starting clean on an architecture you haven’t built for before:
make defconfig
make allnoconfig / allyesconfig / allmodconfig
Useful edge cases for testing:
make allnoconfig # disable everything possible — minimal kernel
make allyesconfig # enable everything possible — maximal kernel, mostly for build testing
make allmodconfig # build everything possible as a module
I use allnoconfig as a starting point when building an extremely minimal kernel for an embedded target, then selectively enable only what that specific hardware needs.
Navigating menuconfig Effectively
Once inside make menuconfig, a few habits make the experience far less overwhelming:
- Use the search function (
/) constantly. Type a keyword likeext4ornvme, and it shows you every matching config symbol along with its exact menu location and dependencies. This is far faster than manually digging through nested menus. - Read the help text (press
?orhon a highlighted option) before enabling something you’re unsure about — it explains what the option does and often what it depends on. - Pay attention to dependency chains. Many options are grayed out until a prerequisite is enabled; the help text will tell you what’s missing.
- Built-in (
y) vs Module (m) — built-in means it’s compiled directly into the kernel image and always available at boot, useful for things needed early (like your root filesystem driver). Module means it’s compiled separately and loaded on demand, useful for things you might not always need, saving memory and boot time.
Configuration Areas Worth Understanding
Processor Type and Features
This section controls CPU-specific optimizations. Setting the correct Processor family matches compiler optimization flags to your actual CPU generation, which can produce a measurable performance improvement over a generic build. There’s also CONFIG_SMP for multi-core support, and CONFIG_PREEMPT options controlling kernel preemption behavior, which matters a lot for latency-sensitive workloads like audio production or real-time control systems.
Filesystem Support
Only enable filesystems you actually use. Every unused filesystem driver is unnecessary attack surface and unnecessary kernel size. I typically keep ext4, xfs, vfat (for EFI system partitions), and overlay (for containers), and strip the rest.
Device Drivers
This is by far the largest section, covering everything from network cards to USB controllers to sound hardware. This is where trimming unused hardware support saves the most kernel size and boot time. If you know your exact target hardware, this is where the biggest wins are.
Kernel Hacking / Debug Options
Contains debugging infrastructure — things like CONFIG_DEBUG_INFO (keeps debug symbols, useful with tools like gdb or crash), CONFIG_KASAN (kernel address sanitizer, catches memory errors, but slows things down significantly), and CONFIG_LOCKDEP (lock dependency validator, invaluable for driver development, but adds overhead). I enable these on development/test kernels and strip them entirely from production builds.
Security Options
Contains CONFIG_SECURITY_SELINUX, CONFIG_SECURITY_APPARMOR, stack protector options, and various hardening features like CONFIG_STRICT_KERNEL_RWX and address space layout randomization support. I always keep these enabled unless I have a very specific reason not to.
Validating a Configuration Before Building
Before kicking off a full build, which can take a long time, I run a sanity check:
make oldconfig
make listnewconfig # shows any options that would be newly introduced
I also often diff my custom config against the distribution’s stock config to see exactly what’s changed, which is invaluable for tracking down a regression later:
diff /boot/config-$(uname -r) .config | less
Saving and Reusing Configurations
Once I have a configuration I’m happy with, I save it as a named defconfig for reuse:
make savedefconfig
cp defconfig arch/x86/configs/myhost_defconfig
Then on future builds against the same source tree or a fresh checkout:
make myhost_defconfig
Example Workflow End to End
Here’s a realistic sequence I follow when preparing a custom configuration for a specific server:
cd linux-6.9
cp /boot/config-$(uname -r) .config
make olddefconfig
make menuconfig
# manually search for and enable/disable specific drivers relevant to this hardware
make savedefconfig
cp defconfig ../myserver_defconfig_$(date +%Y%m%d)
I always keep dated copies of defconfigs in version control, so I can trace exactly what changed between kernel builds over time — this has saved me more than once when a “small tweak” turned out to be the cause of a subtle regression weeks later.
Common Mistakes to Avoid
- Disabling your root filesystem’s driver or your disk controller driver. This is the single most common way to end up with an unbootable kernel. Always double-check filesystem and storage controller support before building.
- Forgetting
CONFIG_MODULES=yif you rely on any loadable modules at all, including third-party drivers like proprietary GPU drivers. - Ignoring dependency warnings during
make oldconfig. If something won’t enable, there’s almost always a missing prerequisite explained in the help text. - Building a debug-heavy kernel for production.
KASAN,LOCKDEP, and heavy debug info options can meaningfully hurt performance; keep those on dedicated test kernels only.
Performance Considerations
A well-tuned configuration can meaningfully reduce boot time and memory footprint, especially on embedded or resource-constrained systems, by excluding entire driver classes you’ll never use. CONFIG_HZ (kernel timer frequency) is another common tuning knob — higher values give finer-grained scheduling at the cost of more timer interrupt overhead, and lower values favor throughput and power efficiency over latency. I set this based on workload: higher HZ for desktop-interactive or audio work, lower for batch/server throughput workloads.
Compatibility Notes Across Distributions
The Kconfig/Kbuild system itself is universal to all upstream and distribution kernels — Debian, Ubuntu, Fedora, RHEL, Arch, and Gentoo all use the exact same underlying mechanism. What differs is packaging: Debian-based systems provide make deb-pkg to produce installable .deb packages from your build, RHEL/Fedora-based systems favor make rpm-pkg or building through rpmbuild with a kernel spec file, and Arch relies on its PKGBUILD conventions. The configuration process I’ve described above is identical regardless of which packaging path you take afterward.
Using Kernel Config Fragments for Modularity
On larger projects or when maintaining several related kernel configurations (say, a family of embedded devices sharing most settings but differing in a handful of driver options), I’ve found config fragments genuinely useful. Rather than maintaining several nearly-identical full .config files, you keep a base config and small fragment files layered on top:
scripts/kconfig/merge_config.sh -m .config fragment-wifi.config fragment-debug.config
make olddefconfig
merge_config.sh, included in the kernel source tree, applies each fragment in order over the base config, and make olddefconfig resolves any newly-introduced dependencies afterward. This approach keeps the differences between related configurations small, explicit, and easy to review in version control, rather than diffing two enormous full config files against each other to spot what actually changed.
Inspecting a Running Kernel’s Actual Configuration
Sometimes I need to check what a kernel that’s already running was actually built with, without having the original .config file handy — useful when investigating a system I didn’t build myself:
zcat /proc/config.gz 2>/dev/null | grep CONFIG_EXT4_FS
If /proc/config.gz isn’t available (it requires CONFIG_IKCONFIG_PROC to have been enabled in that kernel build), check whether the distribution shipped a matching config file alongside the kernel image instead:
zcat /boot/config-$(uname -r) 2>/dev/null | grep CONFIG_EXT4_FS
This is one of my go-to diagnostic steps when troubleshooting a driver or filesystem feature that seems to be missing, since it immediately tells me whether the relevant support was compiled as built-in, as a module, or left out entirely.
Documenting Your Configuration Decisions
For anything beyond a one-off experimental build, I keep a short changelog alongside my saved defconfigs, noting specifically why each non-default option was changed — not just what changed, but the reasoning:
# myserver_defconfig changelog
2026-03-14: Disabled CONFIG_WIRELESS entirely — server has no wifi hardware
2026-03-14: Enabled CONFIG_KASAN for the test-build variant only, disabled in production defconfig
2026-04-02: Enabled CONFIG_BLK_DEV_NVME as built-in rather than module — root filesystem lives on NVMe
Future me, or whoever inherits this system, has consistently thanked past me for this habit — a bare .config diff tells you what changed, but rarely why, and that reasoning is exactly what you need months later when deciding whether a given customization still makes sense to keep.
Summary
Kernel configuration is really about understanding the Kconfig dependency graph and picking a sane starting point rather than building from a blank slate. make menuconfig combined with liberal use of its search function turns a genuinely enormous option space into something manageable. Once you have a working, saved defconfig tailored to your hardware and workload, rebuilding for future kernel versions becomes routine rather than daunting — you’ll layer make oldconfig or make olddefconfig on top of it and move on. This naturally leads into the next step, actually compiling and installing the kernel you’ve configured, which I cover in detail in my kernel rebuild guide.
References
- The Linux Kernel documentation:
Documentation/kbuild/kconfig.rst Documentation/admin-guide/README.rstin the kernel source tree- kernel.org, official upstream kernel source and release notes
- Debian Kernel Handbook, chapter on custom kernel builds
- Red Hat documentation on custom kernel configuration and rpmbuild