Build a GRUB Netboot Directory with grub-mknetdir

Netbooting a fleet of machines means putting GRUB's boot images where TFTP can serve them, and grub-mknetdir builds that tree in one pass. This guide builds a staging copy first, checks it, then leaves publishing to you as a separate, deliberate step. The examples use the installed grub-mknetdir from GRUB 2.12-1ubuntu7.3, supplied by the grub-common package on this machine.

Warning: a repeated run can overwrite files below the destination you give it. Build into a new staging directory first, check the result, then copy or publish the tree during a planned change window rather than pointing the command straight at a live TFTP root.

1. Check the installed command

Three read-only checks, none of which need elevated privileges:

$ command -v grub-mknetdir
/usr/bin/grub-mknetdir
$ grub-mknetdir --version
grub-mknetdir (GRUB) 2.12-1ubuntu7.3
$ dpkg-query -W -f='${Package} ${Version}\n' grub-common
grub-common 2.12-1ubuntu7.3

The manpage describes the result as images under net_directory/subdir, with net_directory treated as the TFTP root. The command has no way to see your server's real TFTP directory, so you always pass it explicitly with --net-directory.

2. Choose a staging root and subdirectory

Set two shell variables, swapping in a real staging path of your own:

$ NETROOT=/srv/tftp-staging
$ SUBDIR=grub-test

Create the staging root if it does not exist yet, then confirm you can actually write to it:

$ mkdir -p "$NETROOT"
$ test -w "$NETROOT" && echo "staging root is writable"
staging root is writable

Creating that staging root is the first command in this guide that changes filesystem state. Use sudo only if your chosen path is not already writable by your account.

Checkpoint: if that test fails, stop and fix ownership or pick a path your account owns. Do not work around a permission problem by writing to the production TFTP root as root instead.

3. Generate the default tree

Run the build with explicit source and destination:

$ grub-mknetdir --net-directory "$NETROOT" --subdir "$SUBDIR"
Netboot directory for i386-pc created. Configure your DHCP server to point to /srv/tftp-staging/grub-test/i386-pc/core.0
Netboot directory for x86_64-efi created. Configure your DHCP server to point to /srv/tftp-staging/grub-test/x86_64-efi/core.efi

Those platform lines are your completion signal, but treat the exact paths as an example: the platform list depends on which GRUB modules are installed on this host. A non-zero exit status means failure, even if some files were written.

Checkpoint: inspect the new tree before it goes anywhere near a client:

$ find "$NETROOT/$SUBDIR" -maxdepth 2 -type f -printf '%P %s bytes\n' | sort | head -20
$ test -s "$NETROOT/$SUBDIR/i386-pc/core.0" && echo "BIOS image exists"
$ test -s "$NETROOT/$SUBDIR/x86_64-efi/core.efi" && echo "EFI image exists"
BIOS image exists
EFI image exists

The BIOS image is core.0, the EFI one is core.efi. Do not rename either just to match a DHCP setting: configure the boot filename to match the path and firmware class your boot service actually expects.

4. Know what the defaults pull in

Left alone, the command uses the GRUB platform data under /usr/lib/grub/<platform>, installs every module, every locale, the unicode font and the starfield theme, with compression left at auto for the core image. That is why a first tree is often much bigger than the two boot images alone.

Run with verbose output when you want an audit trail of exactly what got selected:

$ grub-mknetdir --net-directory "$NETROOT" --subdir "$SUBDIR" --verbose 2> grub-mknetdir.log
$ tail -n 5 grub-mknetdir.log
Netboot directory for i386-pc created. Configure your DHCP server to point to ...
Netboot directory for x86_64-efi created. Configure your DHCP server to point to ...

Keep that log in a file rather than scrolling past it. A line complaining that a locale file could not be opened is not proof the whole tree is broken; check the exit status and the required boot files instead, and investigate before publishing if the exit status is non-zero.

5. Trim the tree once you know what you need

The right module set is a property of your GRUB configuration, not something this command can guess for you. This example keeps only English and drops the theme:

$ grub-mknetdir \
    --net-directory "$NETROOT" \
    --subdir "$SUBDIR-minimal" \
    --locales=en \
    --fonts= \
    --themes=

Empty values are valid syntax, but a minimal tree may be missing files your menu or boot flow needs. Treat this as an experiment: compare it against the default build and test both BIOS and EFI clients before replacing the larger tree.

--modules=MODULES is a different knob again: it pre-loads modules rather than controlling what gets installed. Add one only when your boot design actually calls for it, and verify the result.

6. Add security or platform inputs on purpose

The command can embed a DTB with --dtb=FILE, a public key with --pubkey=FILE, and SBAT metadata with --sbat=FILE. Each of these affects boot trust decisions, so check the ownership, permissions and provenance of the file before you pass it in, and record exactly which file you used.

Security warning: --disable-shim-lock turns off the shim lock verifier. Never reach for it to silence a signature or Secure Boot error; fix the trust chain with whoever owns the platform instead, and test any deliberate change on an isolated client first. A netboot directory is executable boot material, so treat changes to it like any other security-sensitive deployment change.

Only use --directory=DIR when you deliberately want a different GRUB module directory than the installed default. Pointing it somewhere arbitrary can produce a tree that looks complete but is missing the modules your configuration actually needs.

7. Publish only after verification

When replacing an existing tree, keep a rollback copy and publish the new one under a separate subdirectory first. Switch the DHCP or boot-service setting only after clients have tested successfully, and keep the old subdirectory around until everyone agrees rollback is no longer needed.

To abandon a staging build you no longer want, check the path, then remove exactly that directory:

$ printf 'review before deletion: %s\n' "$NETROOT/$SUBDIR"
review before deletion: /srv/tftp-staging/grub-test
$ rm -r -- "$NETROOT/$SUBDIR"

Destructive action: that deletion is irreversible for the staging tree. Never point it at a production TFTP root, and never leave the variable unquoted.

Done means