Right, so you’ve made it past the BIOS/UEFI firmware, and now the baton is passed to the first real software on your system: the bootloader. On most Linux systems, that means GRUB2. Forget the old, simpler GRUB Legacy; GRUB2 is a beast of a different color—significantly more powerful, but with a configuration system that can feel like it was designed by a committee who loved scripts a little too much. Don’t worry, I’ll be your guide through the madness.

Let’s get one thing straight immediately: you do not typically edit the main grub.cfg file by hand. The system will actively fight you on this. That file is generated, and the next time you update a kernel or run grub-mkconfig, your beautiful handwritten changes will be unceremoniously obliterated. The real action happens in the /etc/default/grub file and the myriad of scripts in /etc/grub.d/.

The Two Pillars: /etc/default/grub and /etc/grub.d/

Think of /etc/default/grub as the control panel. It’s a simple key-value file where you set the global variables that the generation scripts will use. This is where you change the timeout, the default boot entry, and pass kernel parameters.

# /etc/default/grub - This is where the sensible people play.

# How long to wait at the menu, in seconds. -1 waits forever.
GRUB_TIMEOUT=5

# The entry to boot. You can use a numeric index (0-indexed!) or the exact menuentry title.
GRUB_DEFAULT=0

# Append these parameters to the kernel command line for EVERY boot.
# This is where you set your root filesystem UUID, quiet splash for a prettier boot, etc.
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"
GRUB_CMDLINE_LINUX=""

# Uncomment to enable booting from a LUKS2-encrypted root. GRUB2 will ask for your password.
#GRUB_ENABLE_CRYPTODISK=y

Now, /etc/grub.d/ is where the magic (or the mess) happens. This directory contains a series of executable scripts—numbered for order—that grub-mkconfig runs to assemble the final grub.cfg. 10_linux looks for your installed kernels, 30_os-prober bravely ventures out to find other operating systems (like that Windows partition you dual-boot with), and 40_custom is your sandbox for manual entries. The numbering is crucial; a script starting with 11_ will run right after 10_linux.

Your Sandbox: The 40_custom File

This is your escape hatch. When the auto-generated scripts just won’t do what you need, you drop your custom menu entries into /etc/grub.d/40_custom. This file is included verbatim in the generated output, so you write raw GRUB2 configuration here.

Let’s say you need to boot a specific old kernel with a special debug parameter, or chainload a weird EFI binary. This is how you do it:

#!/bin/sh
# /etc/grub.d/40_custom - This is your "get out of jail free" card.

exec tail -n +3 $0
# This magic line above lets the script execute itself to generate the config.
# Just ignore it and start writing your menuentries below.

menuentry 'My Old Kernel - Debug Mode' --class gnu-linux --class gnu --class os {
        insmod part_gpt
        insmod ext2
        set root='hd0,gpt2'
        if [ x$feature_platform_search_hint = xy ]; then
          search --no-floppy --fs-uuid --set=root --hint-bios=hd0,gpt2 --hint-efi=hd0,gpt2 --hint-baremetal=ahci0,gpt2  a1b2c3d4-e5f6-7890-abcd-ef1234567890
        else
          search --no-floppy --fs-uuid --set=root a1b2c3d4-e5f6-7890-abcd-ef1234567890
        fi
        echo    'Loading My Old Kernel ...'
        linux   /boot/vmlinuz-5.4.0-999-generic root=UUID=a1b2c3d4-e5f6-7890-abcd-ef1234567890 ro debug=on
        echo    'Loading initial ramdisk ...'
        initrd  /boot/initrd.img-5.4.0-999-generic
}

Notice the sheer verbosity? GRUB2 is painfully explicit. You have to tell it every module to load (insmod) and exactly where to find everything. The search --fs-uuid bit is the robust way to find a partition; it’s what the auto-generated scripts create, and it’s a good practice to avoid your system breaking if drive orders change.

The Moment of Truth: Generating grub.cfg

Once you’ve tweaked your /etc/default/grub and possibly 40_custom, you must synthesize it all into the final configuration. The incantation is always:

sudo grub-mkconfig -o /boot/grub/grub.cfg

This command runs all the scripts in /etc/grub.d/, mixes in the variables from /etc/default/grub, and writes the result to /boot/grub/grub.cfg. Always specify the -o output flag. If you forget it, it will just print the config to stdout, leaving your actual grub.cfg untouched and you wondering why your changes didn’t work. We’ve all done it.

Common Pitfalls and the OS-Prober Menace

The biggest trap is relying on 30_os-prober. It’s notoriously flaky. Sometimes it finds other OSes, sometimes it doesn’t, and sometimes it adds entries in the wrong order. On many distributions, it’s even disabled by default now due to security concerns (imagine a script that automatically probes all your disks—what could go wrong?). If your Windows entry vanishes after a grub-mkconfig, check if 30_os-prober is executable (sudo chmod +x /etc/grub.d/30_os-prober).

Another gotcha: filesystem support. If your /boot is on a Btrfs or ZFS partition, you need the right GRUB2 modules for that. This is often handled by your distribution’s grub package, but it’s a common point of failure if you’re doing something exotic.

The best practice? Keep your customizations in the designated files (/etc/default/grub and 40_custom). Use UUIDs instead of device names like (hd0,1). And for the love of all that is holy, always verify your generated grub.cfg looks correct after running grub-mkconfig. A broken bootloader is a very quiet and lonely place to be.