Configure a Custom Script in the Initrd (Debian 13, GRUB)#
Overview#
Goal: Run your own shell script on the device during early boot (inside the initramfs), for Debian 13 **imageType: raw** images that use GRUB.
This tutorial supports two routes for Debian 13 raw + GRUB images:
Route 1: initramfs-tools flow using
image-templates/debian13-x86_64-bb-raw.ymlRoute 2: dracut module flow using
image-templates/debian13-x86_64-bb-dracut-raw.yml
Both routes achieve the same goal (run your custom logic during early boot), but they use different initrd tooling and file layouts.
Start from your image template: add systemConfig.additionalFiles (and related entries) that point at files in the repo. This page shows the YAML first, then the file contents and boot stages.
Full working examples:
image-templates/debian13-x86_64-bb-raw.ymlwithimage-templates/additionalfiles/debian13-bb/(initramfs-tools flow)image-templates/debian13-x86_64-bb-dracut-raw.ymlwithimage-templates/additionalfiles/debian13-bb-dracut/(dracut module flow)
Choose your route first#
Use exactly one route for your template customization.
Route |
Pick this when |
Start from this template |
|---|---|---|
Route 1: initramfs-tools hook + script |
You want the classic Debian |
|
Route 2: dracut module |
You prefer dracut module structure under |
|
Package switch required for each route#
Route 1 (
bb-raw, initramfs-tools): addinitramfs-toolsandcloud-initramfs-growroot; removedracutanddracut-coreif present.Route 2 (
bb-dracut-raw, dracut): adddracutanddracut-core; removeinitramfs-toolsandcloud-initramfs-growrootif present.For both routes: keep your GRUB and kernel packages (for example
grub-cloud-amd64andlinux-image-amd64).
Remove the current initrd stack with configurations commands#
If your base package set already includes the wrong initrd tooling, remove it explicitly in systemConfig.configurations.
Use one of these snippets based on your route:
If you are switching to Route 1 (initramfs-tools)#
Remove dracut packages and dracut module config:
configurations:
- cmd: "apt-get purge -y dracut dracut-core || true"
- cmd: "rm -f /etc/dracut.conf.d/*.conf"
If you are switching to Route 2 (dracut)#
Remove initramfs-tools packages and old hook/script paths:
configurations:
- cmd: "apt-get purge -y initramfs-tools cloud-initramfs-growroot || true"
- cmd: "rm -f /etc/initramfs-tools/hooks/hello /etc/initramfs-tools/scripts/init-bottom/hello"
Notes:
Keep only one initrd framework in the final image to avoid mixed behavior.
Keep your selected framework in
packages(initramfs-toolsfor Route 1, ordracut+dracut-corefor Route 2).|| truemakes the command safe when a package is not installed.
Route 1: initramfs-tools hook + boot script (bb-raw)#
Use debian13-x86_64-bb-raw.yml as an example.
Paths in **local** are relative to the directory that contains your template YAML (image-templates/… → additionalfiles/debian13-bb/…).
...
bootloader:
bootType: efi
provider: grub
packages:
- initramfs-tools
# … keep your other packages (see bb example)
additionalFiles:
- local: additionalfiles/debian13-bb/hello.sh
final: /usr/local/sbin/hello.sh
- local: additionalfiles/debian13-bb/hooks/hello
final: /etc/initramfs-tools/hooks/hello
- local: additionalfiles/debian13-bb/scripts/init-bottom/hello
final: /etc/initramfs-tools/scripts/init-bottom/hello
configurations:
- cmd: "chmod 755 /usr/local/sbin/hello.sh /etc/initramfs-tools/hooks/hello /etc/initramfs-tools/scripts/init-bottom/hello"
What each additionalFiles entry does#
|
|
Role |
|---|---|---|
|
|
Your script on the rootfs; the hook copies it into the initramfs. |
|
|
Runs when |
|
|
Runs on the device during early boot (execute step). |
**local:** file on the build host (must exist before compose).**final:** path on the image; ICT copies files here before GRUB install runs**update-initramfs**.
Rename debian13-bb in local paths to match your folder name. Keep the **final** paths as shown.
Field summary#
Template field |
Purpose |
|---|---|
|
Required. Installs hook, boot script, and your |
|
Include |
|
|
Route 2: dracut module (bb-dracut-raw)#
If you want the same early-boot marker behavior using dracut modules instead of initramfs-tools hooks, use debian13-x86_64-bb-dracut-raw.yml.
This variant keeps the same Debian 13 + GRUB + raw image target but changes how content is added to initrd. The shipped template also layers on a GNOME/X11 desktop (GDM login) — the table below covers only the initrd-handling difference between the two routes, not the full package set.
Area |
|
|
|---|---|---|
Package focus |
|
|
Files copied by template |
|
|
Destination inside image |
|
|
Enable step |
Hook/script are discovered by initramfs-tools layout |
Add |
dracut template snippet#
packages:
- dracut
- dracut-core
# ... keep your GRUB/kernel packages
additionalFiles:
- local: additionalfiles/debian13-bb-dracut/modules.d/91hello/module-setup.sh
final: /usr/lib/dracut/modules.d/91hello/module-setup.sh
- local: additionalfiles/debian13-bb-dracut/modules.d/91hello/hello.sh
final: /usr/lib/dracut/modules.d/91hello/hello.sh
- local: additionalfiles/debian13-bb-dracut/modules.d/91hello/initqueue-sample.sh
final: /usr/lib/dracut/modules.d/91hello/initqueue-sample.sh
configurations:
- cmd: 'mkdir -p /etc/dracut.conf.d && echo ''add_dracutmodules+=" hello "'' > /etc/dracut.conf.d/91hello.conf'
- cmd: "chmod 755 /usr/lib/dracut/modules.d/91hello/module-setup.sh /usr/lib/dracut/modules.d/91hello/hello.sh /usr/lib/dracut/modules.d/91hello/initqueue-sample.sh"
- cmd: "printf 'Package: systemd-boot systemd-boot-efi\\nPin: release *\\nPin-Priority: -1\\n' > /etc/apt/preferences.d/no-systemd-boot"
- cmd: "dpkg --purge --force-all systemd-boot systemd-boot-efi 2>/dev/null || true"
Debian 13 raw templates inherit systemd-boot from the OS default package list even when bootloader.provider is grub. Purge it so initramfs regeneration does not run the systemd-boot kernel/install.d hook (which fails when dracut writes the initrd directly under /boot/efi/...).
dracut module files#
Create the module under:
image-templates/
additionalfiles/debian13-bb-dracut/
modules.d/91hello/
module-setup.sh
hello.sh
initqueue-sample.sh
module-setup.sh declares install logic for the module; hello.sh is the script executed from initrd.
Use the provided example files in image-templates/additionalfiles/debian13-bb-dracut/modules.d/91hello/ as the
reference implementation.
dracut initqueue stage example (initqueue-sample.sh)#
Use this when you want a script to run in dracut’s initqueue phase while root-device discovery is still in progress.
In this example module:
module-setup.shinstallsinitqueue-sample.shinto initrd as/sbin/initqueue-sample.sh.The script is registered in two hook points:
inst_hook cmdline 5 ...to seed an initial initqueue job early.inst_hook initqueue 90 ...to run in initqueue rounds.
Key behavior of initqueue-sample.sh:
Logs markers such as
WAIT_ROOT_EXECUTED,WAIT_ROOT_REQUEUE, andWAIT_ROOT_MAX_ROUNDS_REACHED.Reads
root=from kernel cmdline and resolves common forms (/dev/...,UUID=...,LABEL=...,PARTUUID=...).Requeues itself with
initqueue --onetimefor up to 3 rounds if the root block device is not yet present.Adds a settled readiness check with
initqueue --settled /bin/sh -c "test -b \"$ROOTDEV\"".
Add this file in your template when using the example module:
additionalFiles:
- local: additionalfiles/debian13-bb-dracut/modules.d/91hello/initqueue-sample.sh
final: /usr/lib/dracut/modules.d/91hello/initqueue-sample.sh
How to verify on boot:
Check
dmesgforWAIT_ROOT_markers.If available in the initramfs runtime, inspect
/run/initramfs/wait-root.log.
Why this is different from the ad-hoc install_items initqueue approach#
You may have seen another pattern where files are copied into initramfs paths and then listed in dracut
install_items so they get packed. That method can work, but it is different from this tutorial’s approach.
This tutorial uses a dracut module + template-declared files approach, which gives you:
Reproducibility in ICT: all inputs are declared in the image template (
additionalFiles) and tracked as part of the image definition, instead of relying on one-off build-host actions.Cleaner dracut lifecycle integration:
module-setup.shdefines exactly which hooks are used (cmdline,initqueue,pre-mount) and in what order, so behavior is explicit and reviewable.Correct behavior for event-driven initqueue: dracut
initqueueis event/timing driven, so a sample script must be registered as a hook and be able to requeue/wait for readiness signals; only listing files withinstall_itemspackages content into initrd but does not model this runtime event flow by itself.Better portability across build environments: module files live in the repo and are copied into the target image layout; the flow does not depend on host-specific runtime state.
Easier maintenance: script logic (for example, requeue rounds and root-device checks in
initqueue-sample.sh) stays in one place, rather than being split across ad-hoc file install and pack lists.
In short: both methods can place a script in initrd, but the module-based method used here is preferred for declarative, version-controlled image builds in ICT.
Running a script once, on the full OS’s first boot (not initrd)#
The routes above run a script inside initrd, before the real root filesystem takes over. If you instead want a script that runs once, after the device has switched to the installed OS, on its first boot only, use a systemd oneshot unit gated by a marker file instead of a dracut module.
Full working example: image-templates/debian13-x86_64-bb-dracut-raw.yml with
image-templates/additionalfiles/debian13-bb-dracut/usr/local/sbin/first-boot-sample.sh and
.../etc/systemd/system/first-boot-sample.service.
additionalFiles:
- local: additionalfiles/debian13-bb-dracut/usr/local/sbin/first-boot-sample.sh
final: /usr/local/sbin/first-boot-sample.sh
- local: additionalfiles/debian13-bb-dracut/etc/systemd/system/first-boot-sample.service
final: /etc/systemd/system/first-boot-sample.service
configurations:
- cmd: "chmod 755 /usr/local/sbin/first-boot-sample.sh"
- cmd: 'systemctl enable first-boot-sample.service'
The unit’s ConditionPathExists=!/var/lib/first-boot-sample.done skips ExecStart once the marker file exists;
ExecStartPost creates that marker after a successful run. The unit stays WantedBy=multi-user.target on every
boot (so it evaluates the condition each time), but the script itself only ever executes once. This is preferred
over having the script disable its own unit, which races with RemainAfterExit=yes.
To see the message on the serial console / dmesg, not just journalctl, the script mirrors it to /dev/kmsg
(this template’s kernel cmdline sets console=ttyS0,115200) and the unit sets StandardOutput=journal+console.
Build and check#
Build the tool, install prerequisites, validate, and compose the image using the README.md (Quick Start and Compose an Image).
Pick one template based on your chosen route:
image-templates/debian13-x86_64-bb-raw.ymlimage-templates/debian13-x86_64-bb-dracut-raw.yml
Run validate if you use it (see Usage Guide). If validate warns about a missing local file, fix the path or add the file under Where to put files in the repo.
On the device:
Boot the flashed raw image.
Use serial console if your template sets
console=ttyS0,...on the kernel cmdline.Look for your message during early boot, or run:
dmesg | grep -i hello
Optional checks on a machine with the image mounted:
initramfs-tools flow:
lsinitramfs /boot/initrd.img-* | grep hellodracut flow:
lsinitrd /boot/initrd.img-* | grep 91hello
Where to put files in the repo#
Layout |
|
|---|---|
Next to templates (recommended) |
|
Debian OS defaults tree |
|
Example tree:
image-templates/
debian13-x86_64-bb-raw.yml
additionalfiles/debian13-bb/
hello.sh
hooks/hello
scripts/init-bottom/hello
Supporting files (content to create)#
hello.sh#
#!/bin/sh
echo "hello from initrd (debian13-bb)" >/dev/kmsg
Use /dev/kmsg or logger so output appears on serial or in dmesg.
hooks/hello#
Runs during **update-initramfs** on the build machine; copies hello.sh into the initramfs image.
#!/bin/sh
PREREQ=""
prereqs() { echo "$PREREQ"; exit 0; }
case "$1" in
prereqs) prereqs; exit 0 ;;
esac
. /usr/share/initramfs-tools/hook-functions
copy_exec /usr/local/sbin/hello.sh /usr/local/sbin/hello.sh
scripts/init-bottom/hello#
Runs on the device in the initrd (default: late in initramfs, before switch to the installed system).
#!/bin/sh
PREREQ=""
prereqs() { echo "$PREREQ"; exit 0; }
case "$1" in
prereqs) prereqs; exit 0 ;;
esac
if [ -x /usr/local/sbin/hello.sh ]; then
/usr/local/sbin/hello.sh
fi
Make all three executable (chmod 755) or use the configurations line in the template.
Every initramfs-tools hook and script must start with the **PREREQ / prereqs** block shown above.
Choosing an initramfs-tools boot stage#
The hook always runs at image build time when the initramfs is generated. To change when your script runs on the device, move the runner to a different directory under scripts/ (and update the template final: path).
|
When it runs (plain language) |
Good for |
|---|---|---|
|
Late initrd, after root handling, before switch_root. Default in the example. |
Logging, checks before the real OS starts. |
|
Early, before mounting root |
Very early setup |
|
Before local root mount |
Block device ready, root not mounted yet |
|
After local root mount steps |
Work that needs the root filesystem mounted in initrd |
There is no 99 prefix naming rule; the file name (hello) is arbitrary.
Troubleshooting#
Problem |
Check |
|---|---|
No output on boot |
All three |
Validate / build skips a file |
Wrong |
Script on disk but not in initrd |
Missing |
Script in initramfs but never runs |
Missing or wrong |
Wrong image type or bootloader |
This guide targets Debian 13 raw with |
|
|