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:

  1. Route 1: initramfs-tools flow using image-templates/debian13-x86_64-bb-raw.yml

  2. Route 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.yml with image-templates/additionalfiles/debian13-bb/ (initramfs-tools flow)

  • image-templates/debian13-x86_64-bb-dracut-raw.yml with image-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 update-initramfs hook model (hooks/ + scripts/init-bottom/).

image-templates/debian13-x86_64-bb-raw.yml

Route 2: dracut module

You prefer dracut module structure under modules.d/ and dracut module enablement config.

image-templates/debian13-x86_64-bb-dracut-raw.yml

Package switch required for each route#

  • Route 1 (bb-raw, initramfs-tools): add initramfs-tools and cloud-initramfs-growroot; remove dracut and dracut-core if present.

  • Route 2 (bb-dracut-raw, dracut): add dracut and dracut-core; remove initramfs-tools and cloud-initramfs-growroot if present.

  • For both routes: keep your GRUB and kernel packages (for example grub-cloud-amd64 and linux-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-tools for Route 1, or dracut + dracut-core for Route 2).

  • || true makes 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#

local (your repo)

final (inside the built image)

Role

…/hello.sh

/usr/local/sbin/hello.sh

Your script on the rootfs; the hook copies it into the initramfs.

…/hooks/hello

/etc/initramfs-tools/hooks/hello

Runs when **update-initramfs** builds the initramfs (pack step).

…/scripts/init-bottom/hello

/etc/initramfs-tools/scripts/init-bottom/hello

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

additionalFiles

Required. Installs hook, boot script, and your hello.sh.

packages

Include **initramfs-tools** (and **grub-cloud-amd64** or your GRUB packages).

bootloader.provider

**grub** for this guide (initramfs is rebuilt via update-initramfs).


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

debian13-x86_64-bb-raw.yml (initramfs-tools)

debian13-x86_64-bb-dracut-raw.yml (dracut)

Package focus

initramfs-tools

dracut and dracut-core

Files copied by template

hello.sh, hooks/hello, scripts/init-bottom/hello

modules.d/91hello/module-setup.sh, modules.d/91hello/hello.sh, modules.d/91hello/initqueue-sample.sh

Destination inside image

/etc/initramfs-tools/... and /usr/local/sbin/hello.sh

/usr/lib/dracut/modules.d/91hello/...

Enable step

Hook/script are discovered by initramfs-tools layout

Add /etc/dracut.conf.d/91hello.conf with add_dracutmodules+=" hello "

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.sh installs initqueue-sample.sh into 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, and WAIT_ROOT_MAX_ROUNDS_REACHED.

  • Reads root= from kernel cmdline and resolves common forms (/dev/..., UUID=..., LABEL=..., PARTUUID=...).

  • Requeues itself with initqueue --onetime for 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 dmesg for WAIT_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.sh defines 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 initqueue is 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 with install_items packages 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.yml

  • image-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:

  1. Boot the flashed raw image.

  2. Use serial console if your template sets console=ttyS0,... on the kernel cmdline.

  3. 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 hello

  • dracut flow: lsinitrd /boot/initrd.img-* | grep 91hello


Where to put files in the repo#

Layout

local in template

Next to templates (recommended)

image-templates/additionalfiles/<your-name>/...

Debian OS defaults tree

../additionalfiles/... from config/osv/debian/debian13/imageconfigs/defaultconfigs/

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).

final path under /etc/initramfs-tools/scripts/

When it runs (plain language)

Good for

**init-bottom/hello**

Late initrd, after root handling, before switch_root. Default in the example.

Logging, checks before the real OS starts.

init-premount

Early, before mounting root

Very early setup

local-premount

Before local root mount

Block device ready, root not mounted yet

local-bottom

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 additionalFiles entries; chmod 755 on hook and scripts; initramfs-tools in packages.

Validate / build skips a file

Wrong local path relative to the template YAML.

Script on disk but not in initrd

Missing hooks/hello or hook not executable.

Script in initramfs but never runs

Missing or wrong scripts/.../hello path; wrong boot stage directory.

Wrong image type or bootloader

This guide targets Debian 13 raw with **bootloader: grub**.

dracut -f exits 1 after initrd is built; install: ... are the same file

systemd-boot is still installed (default merge). Purge it (see Route 2 snippet) or run dracut --force --kver $(uname -r) /boot/initrd.img-$(uname -r) then update-grub.