These are steps that I used on an Ubuntu 18.04 LTS (Bionic Beaver) 64-bit system to build an x86 Debian 9 (Stretch) live environment that I can boot from CD or USB.

These steps can be used to create a live environment that is BIOS bootable (MBR), UEFI bootable (GPT), or combination of both UEFI and BIOS bootable. Something unique about this guide is that Syslinux/Isolinux are not used. Only Grub boot equipment. This is done for consistency and to avoid mixing the two (Syslinux/Isolinux alone cannot accomplish everything covered in this article, but Grub can).

Here are some alternatives to my guide that may be better solutions for anyone reading this article: live-build, mkusb, UNetbootin, xixer, rufus, YUMI, Simple-cdd. You should also look at the Debian DebianCustomCD documentation as it will prove more informative than this article could ever be.

I wrote this guide more for educational purposes than anything. It is not necessarily the fastest guide or the best guide for your needs. I hope it is helpful all the same.

Warning: I have highlighted all the places you should be in the [chroot] environment. Be careful! Running some of these commands on your local environment instead of in the chroot can damage your system.

Prerequisites

Install applications we need to build the environment.

sudo apt-get install \
    debootstrap \
    squashfs-tools \
    xorriso \
    grub-pc-bin \
    grub-efi-amd64-bin \
    mtools

Create a directory where we will store all of the files we create throughout this guide.

mkdir $HOME/LIVE_BOOT

Bootstrap and Configure Debian

Set up the base Debian environment. I am using stretch for my distribution and i386 for the architecture. Consult the list of debian mirrors.

Please change the URL in this command if there is a mirror that is closer to you.

sudo debootstrap \
    --arch=i386 \
    --variant=minbase \
    stretch \
    $HOME/LIVE_BOOT/chroot \
    http://ftp.us.debian.org/debian/

Chroot to the Debian environment we just bootstrapped.

sudo chroot $HOME/LIVE_BOOT/chroot

[chroot] Set a custom hostname for your Debian environment.

echo "debian-live" > /etc/hostname

[chroot] Figure out which Linux Kernel you want in the live environment.

apt-cache search linux-image

[chroot] I chose the image linux-image-686. I also believe live-boot is a necessity. systemd-sys (or an equivalent) is also necessary to provide init.

apt-get update && \
apt-get install --no-install-recommends \
    linux-image-686 \
    live-boot \
    systemd-sysv

[chroot] Install programs of your choosing, and then run apt-get clean to save some space. I use --no-install-recommends to avoid superfluous packages. You should decide what you need for your environment.

Read Debian’s ReduceDebian article for tips on reducing the size of your Debian environment if size is important and you want a minimal and compact installation. Please note that some live environments like Tiny Core Linux or Puppy Linux are specifically optimized for a tiny footprint. Although this article results in a relatively tiny live environment, generating an environment that is only a couple dozen MB large takes additional effort not covered in this article.

apt-get install --no-install-recommends \
    network-manager net-tools wireless-tools wpagui \
    curl openssh-client \
    blackbox xserver-xorg-core xserver-xorg xinit xterm \
    nano && \
apt-get clean

[chroot] Set the root password. root will be the only user in this live environment by default, but you may add additional users as needed.

passwd root

[chroot] Exit the chroot.

exit

Create directories that will contain files for our live environment files and scratch files.

mkdir -p $HOME/LIVE_BOOT/{scratch,image/live}

Compress the chroot environment into a Squash filesystem.

sudo mksquashfs \
    $HOME/LIVE_BOOT/chroot \
    $HOME/LIVE_BOOT/image/live/filesystem.squashfs \
    -e boot

Copy the kernel and initramfs from inside the chroot to the live directory.

cp $HOME/LIVE_BOOT/chroot/boot/vmlinuz-* \
    $HOME/LIVE_BOOT/image/vmlinuz && \
cp $HOME/LIVE_BOOT/chroot/boot/initrd.img-* \
    $HOME/LIVE_BOOT/image/initrd

Create a menu configuration file for grub. Please note that the insmod all_video line was needed in my testing to deal with a bug in UEFI booting for one of my machines. Perhaps not everyone will need that line, but I did.

This config instructs Grub to use the search command to infer which device contains our live environment. This seems like the most portable solution considering the various ways we may write our live environment to bootable media.

cat <<'EOF' >$HOME/LIVE_BOOT/scratch/grub.cfg

search --set=root --file /DEBIAN_CUSTOM

insmod all_video

set default="0"
set timeout=30

menuentry "Debian Live" {
    linux /vmlinuz boot=live quiet nomodeset
    initrd /initrd
}
EOF

Create a special file in image named DEBIAN_CUSTOM. This file will be used to help Grub figure out which device contains our live filesystem. This file name must be unique and must match the file name in our grub.cfg config.

touch $HOME/LIVE_BOOT/image/DEBIAN_CUSTOM

Your LIVE_BOOT directory should now roughly look like this.

LIVE_BOOT/chroot/*tons of chroot files*
LIVE_BOOT/scratch/grub.cfg
LIVE_BOOT/image/DEBIAN_CUSTOM
LIVE_BOOT/image/initrd
LIVE_BOOT/image/vmlinuz
LIVE_BOOT/image/live/filesystem.squashfs

Create a bootable medium

Please note that there are two separate sets of instructions below for creating a bootable medium for the live environment. One process is titled Create Bootable ISO/CD and the other, separate process, is titled Create Bootable USB.

  • The Create Bootable ISO/CD instructions will result in an .iso image file containing our live environment.
  • The Create Bootable USB instructions will result in our live environment being installed directly to a USB device.

The .iso file we create with the Create Bootable ISO/CD instructions can be burned to a CD-ROM (optical media), or written to a USB device with dd. The functionality that allows this “dd-able” behavior from our .iso file does not come for free. The process is a bit complex, but that resultant behavior is common in many modern live environments such as the Ubuntu installation .iso file.

Please note that writing an .iso file to a USB device is not the same as installing the live environment directly to a USB device. Read more in my Notes which details my discoveries.

Create Bootable ISO/CD

Install the live environment to an .iso file which can be burned to optical media.

As stated above, the .iso file generated by these steps can be written to a USB device with something like dd.

I have separated the instructions for creating a BIOS bootable, UEFI bootable, or combination BIOS + UEFI bootable .iso in order to illustrate the separate processes. Click either “BIOS”, “UEFI”, or “BIOS + UEFI” and follow the instructions that are appropriate for your needs.

Create a grub BIOS image.

grub-mkstandalone \
    --format=i386-pc \
    --output=$HOME/LIVE_BOOT/scratch/core.img \
    --install-modules="linux normal iso9660 biosdisk memdisk search tar ls" \
    --modules="linux normal iso9660 biosdisk search" \
    --locales="" \
    --fonts="" \
    "boot/grub/grub.cfg=$HOME/LIVE_BOOT/scratch/grub.cfg"

Use cat to combine a bootable Grub cdboot.img bootloader with our boot image.

cat \
    /usr/lib/grub/i386-pc/cdboot.img \
    $HOME/LIVE_BOOT/scratch/core.img \
> $HOME/LIVE_BOOT/scratch/bios.img

Generate the ISO file.

xorriso \
    -as mkisofs \
    -iso-level 3 \
    -full-iso9660-filenames \
    -volid "DEBIAN_CUSTOM" \
    --grub2-boot-info \
    --grub2-mbr /usr/lib/grub/i386-pc/boot_hybrid.img \
    -eltorito-boot \
        boot/grub/bios.img \
        -no-emul-boot \
        -boot-load-size 4 \
        -boot-info-table \
        --eltorito-catalog boot/grub/boot.cat \
    -output "${HOME}/LIVE_BOOT/debian-custom.iso" \
    -graft-points \
        "${HOME}/LIVE_BOOT/image" \
        /boot/grub/bios.img=$HOME/LIVE_BOOT/scratch/bios.img

Now burn the ISO to a CD and you should be ready to boot from it using a BIOS system.

Create a grub UEFI image.

grub-mkstandalone \
    --format=x86_64-efi \
    --output=$HOME/LIVE_BOOT/scratch/bootx64.efi \
    --locales="" \
    --fonts="" \
    "boot/grub/grub.cfg=$HOME/LIVE_BOOT/scratch/grub.cfg"

Create a FAT16 UEFI boot disk image containing the EFI bootloader. Note the use of the mmd and mcopy commands to copy our UEFI boot loader named bootx64.efi.

(cd $HOME/LIVE_BOOT/scratch && \
    dd if=/dev/zero of=efiboot.img bs=1M count=10 && \
    mkfs.vfat efiboot.img && \
    mmd -i efiboot.img efi efi/boot && \
    mcopy -i efiboot.img ./bootx64.efi ::efi/boot/
)

Generate the ISO file.

xorriso \
    -as mkisofs \
    -iso-level 3 \
    -full-iso9660-filenames \
    -volid "DEBIAN_CUSTOM" \
    -eltorito-alt-boot \
        -e EFI/efiboot.img \
        -no-emul-boot \
    -append_partition 2 0xef ${HOME}/LIVE_BOOT/scratch/efiboot.img \
    -output "${HOME}/LIVE_BOOT/debian-custom.iso" \
    -graft-points \
        "${HOME}/LIVE_BOOT/image" \
        /EFI/efiboot.img=$HOME/LIVE_BOOT/scratch/efiboot.img

Now burn the ISO to a CD and you should be ready to boot from it using a UEFI system.

Create a grub UEFI image.

grub-mkstandalone \
    --format=x86_64-efi \
    --output=$HOME/LIVE_BOOT/scratch/bootx64.efi \
    --locales="" \
    --fonts="" \
    "boot/grub/grub.cfg=$HOME/LIVE_BOOT/scratch/grub.cfg"

Create a FAT16 UEFI boot disk image containing the EFI bootloader. Note the use of the mmd and mcopy commands to copy our UEFI boot loader named bootx64.efi.

(cd $HOME/LIVE_BOOT/scratch && \
    dd if=/dev/zero of=efiboot.img bs=1M count=10 && \
    mkfs.vfat efiboot.img && \
    mmd -i efiboot.img efi efi/boot && \
    mcopy -i efiboot.img ./bootx64.efi ::efi/boot/
)

Create a grub BIOS image.

grub-mkstandalone \
    --format=i386-pc \
    --output=$HOME/LIVE_BOOT/scratch/core.img \
    --install-modules="linux normal iso9660 biosdisk memdisk search tar ls" \
    --modules="linux normal iso9660 biosdisk search" \
    --locales="" \
    --fonts="" \
    "boot/grub/grub.cfg=$HOME/LIVE_BOOT/scratch/grub.cfg"

Use cat to combine a bootable Grub cdboot.img bootloader with our boot image.

cat \
    /usr/lib/grub/i386-pc/cdboot.img \
    $HOME/LIVE_BOOT/scratch/core.img \
> $HOME/LIVE_BOOT/scratch/bios.img

Generate the ISO file.

xorriso \
    -as mkisofs \
    -iso-level 3 \
    -full-iso9660-filenames \
    -volid "DEBIAN_CUSTOM" \
    -eltorito-boot \
        boot/grub/bios.img \
        -no-emul-boot \
        -boot-load-size 4 \
        -boot-info-table \
        --eltorito-catalog boot/grub/boot.cat \
    --grub2-boot-info \
    --grub2-mbr /usr/lib/grub/i386-pc/boot_hybrid.img \
    -eltorito-alt-boot \
        -e EFI/efiboot.img \
        -no-emul-boot \
    -append_partition 2 0xef ${HOME}/LIVE_BOOT/scratch/efiboot.img \
    -output "${HOME}/LIVE_BOOT/debian-custom.iso" \
    -graft-points \
        "${HOME}/LIVE_BOOT/image" \
        /boot/grub/bios.img=$HOME/LIVE_BOOT/scratch/bios.img \
        /EFI/efiboot.img=$HOME/LIVE_BOOT/scratch/efiboot.img

Now burn the ISO to a CD and you should be ready to boot from it using either a UEFI or a BIOS system.

Create Bootable USB

Install the live environment to a USB device.

As stated above, installing a live environment to a USB device is not the same as writing an .iso file to a USB device. The end result is the same for the most part in both those scenarios, but there are subtle differences worth understanding, and there are valid reasons someone may want to install a live environment directly to a USB device rather than writing an .iso file to a USB device.

I have separated the instructions for creating a BIOS bootable, UEFI bootable, or combination BIOS + UEFI bootable USB device in order to illustrate the separate processes. Click either “BIOS”, “UEFI”, or “BIOS + UEFI” and follow the instructions that are appropriate for your needs.

I am assuming you have an umounted blank USB drive at /dev/sdz. To allow easily swapping in a real block device, I am using a variable named $disk in these commands.

Export the disk variable.

export disk=/dev/sdz

Create a mount point for the USB drive.

sudo mkdir -p /mnt/usb

Partition the USB drive using parted. This command creates 1 partition in a traditional MBR layout.

sudo parted --script $disk \
    mklabel msdos \
    mkpart primary fat32 1MiB 100%

Format the partition.

sudo mkfs.vfat -F32 ${disk}1

Mount the partition.

sudo mount ${disk}1 /mnt/usb

Install Grub for i386-pc booting.

sudo grub-install \
    --target=i386-pc \
    --boot-directory=/mnt/usb/boot \
    --recheck \
    $disk

Create a live directory on the USB device.

sudo mkdir -p /mnt/usb/{boot/grub,live}

Copy the Debian live environment files we previously generated to the USB disk.

sudo cp -r $HOME/LIVE_BOOT/image/* /mnt/usb/

Copy the grub.cfg boot configuration to the USB device.

sudo cp \
    $HOME/LIVE_BOOT/scratch/grub.cfg \
    /mnt/usb/boot/grub/grub.cfg

Now unmount the disk and you should be ready to boot from it on a BIOS system.

sudo umount /mnt/usb

I am assuming you have an umounted blank USB drive at /dev/sdz. To allow easily swapping in a real block device, I am using a variable named $disk in these commands.

Export the disk variable.

export disk=/dev/sdz

Create some mount points for the USB drive.

sudo mkdir -p /mnt/{usb,efi}

Partition the USB drive using parted. This command creates 2 partitions in a GPT (Guid Partition Table) layout. One partition for UEFI, and one for our Debian OS and other live data.

sudo parted --script $disk \
    mklabel gpt \
    mkpart ESP fat32 1MiB 200MiB \
        name 1 EFI \
        set 1 esp on \
    mkpart primary fat32 200MiB 100% \
        name 2 LINUX \
        set 2 msftdata on

Format the UEFI and data partitions.

sudo mkfs.vfat -F32 ${disk}1 && \
sudo mkfs.vfat -F32 ${disk}2

Mount the partitions.

sudo mount ${disk}1 /mnt/efi && \
sudo mount ${disk}2 /mnt/usb

Install Grub for x86_64 UEFI booting.

sudo grub-install \
    --target=x86_64-efi \
    --efi-directory=/mnt/efi \
    --boot-directory=/mnt/usb/boot \
    --removable \
    --recheck

Create a live directory on the USB device.

sudo mkdir -p /mnt/usb/{boot/grub,live}

Copy the Debian live environment files we previously generated to the USB disk.

sudo cp -r $HOME/LIVE_BOOT/image/* /mnt/usb/

Copy the grub.cfg boot configuration to the USB device.

sudo cp \
    $HOME/LIVE_BOOT/scratch/grub.cfg \
    /mnt/usb/boot/grub/grub.cfg

Now unmount the disk and you should be ready to boot from it on a UEFI system.

sudo umount /mnt/{usb,efi}

I am assuming you have an umounted blank USB drive at /dev/sdz. To allow easily swapping in a real block device, I am using a variable named $disk in these commands.

Export the disk variable.

export disk=/dev/sdz

Create some mount points for the USB drive.

sudo mkdir -p /mnt/{usb,efi}

Partition the USB drive using parted. This command creates 3 partitions in a GPT (Guid Partition Table) layout. One partition for the BIOS boot record, one for UEFI, and one for our Debian OS and other live data.

sudo parted --script $disk \
    mklabel gpt \
    mkpart primary fat32 2048s 4095s \
        name 1 BIOS \
        set 1 bios_grub on \
    mkpart ESP fat32 4096s 413695s \
        name 2 EFI \
        set 2 esp on \
    mkpart primary fat32 413696s 100% \
        name 3 LINUX \
        set 3 msftdata on

Generate a hybrid MBR for the USB device. Note, this is non-standard and so may not work on all systems. The only guides I’ve found on hybrid MBRs show that it must be done with gdisk. gdisk supports commands not in sgdisk, so this command is not easily scriptable. The documentation for gdisk warn that this procedure is non-standard, flaky, and unsupported, but this does generally seem to do what is expected. It allows for both BIOS and UEFI booting from the same USB device.

sudo gdisk $disk << EOF
r     # recovery and transformation options
h     # make hybrid MBR
1 2 3 # partition numbers for hybrid MBR
N     # do not place EFI GPT (0xEE) partition first in MBR
EF    # MBR hex code
N     # do not set bootable flag
EF    # MBR hex code
N     # do not set bootable flag
83    # MBR hex code
Y     # set the bootable flag
x     # extra functionality menu
h     # recompute CHS values in protective/hybrid MBR
w     # write table to disk and exit
Y     # confirm changes
EOF

Format the UEFI and data partitions.

sudo mkfs.vfat -F32 ${disk}2 && \
sudo mkfs.vfat -F32 ${disk}3

Mount the partitions.

sudo mount ${disk}2 /mnt/efi && \
sudo mount ${disk}3 /mnt/usb

Install Grub for x86_64 UEFI booting.

sudo grub-install \
    --target=x86_64-efi \
    --efi-directory=/mnt/efi \
    --boot-directory=/mnt/usb/boot \
    --removable \
    --recheck

Install Grub for i386-pc booting.

sudo grub-install \
    --target=i386-pc \
    --boot-directory=/mnt/usb/boot \
    --recheck \
    $disk

Create a live directory on the USB device.

sudo mkdir -p /mnt/usb/{boot/grub,live}

Copy the Debian live environment files we previously generated to the USB disk.

sudo cp -r $HOME/LIVE_BOOT/image/* /mnt/usb/

Copy the grub.cfg boot configuration to the USB device.

sudo cp \
    $HOME/LIVE_BOOT/scratch/grub.cfg \
    /mnt/usb/boot/grub/grub.cfg

Now unmount the disk and you should be ready to boot from it on either a BIOS or UEFI system.

sudo umount /mnt/{usb,efi}

Notes +

Please note that the result of writing an .iso file generated by the ISO/CD instructions to a USB device is not the same as the other set of instructions for Direct USB.

If you write an .iso to a USB device you are deleting all the data (including partitions/filesystem) on that USB device and replacing them with a read-only iso9660 filesystem, which is normally what we see on CDs! On the other hand, if we use the Direct USB instructions, then the USB device will have traditional partitions that we can read from and write to on any standard machine.

Please read in entirety the syslinux isohybrid MBR article and the El-Torito article on Hybrid ISO booting and their related articles. The Debian xorriso man page has excellent documentation around ISO/USB booting as well.

Syslinux/Isolinux are more commonly used when creating an .iso that can also be written directly to USB, but the Debian xorrisofs docs say the following.

EFI boot equipment may be combined with above ISOLINUX isohybrid for PC-BIOS in a not really UEFI-2.4 compliant way…

I don’t like that not really...compliant bit.

Though, the docs also point out that it does work well. However, I’m trying to consolidate on Grub to avoid a mix of Syslinux configs/files and Grub commands/files.

Figuring out the right syntax and bootloaders to use for each command was a nightmare, and took a lot of time and reading and trial and error. I realize there are simpler commands like grub-mkrescue, but I wanted to explicitly call out the manual commands used for each process.

The GNU xorrisofs docs seem outdated. The Debian docs were more reliable with better examples.

Examining the grub-mkrescue source code and this xorriso mailing list exchange helped me figure out how to use the --grub2-mbr flag to install the standard MBR bootloader in the .iso file.

Supposedly, we can also use -partition_offset so the .iso can be written to USB to help for natural disk layouts on the USB device, but I could not figure out how to get that working.

… facilitates later manipulations of the USB stick by tools for partitioning and formatting…

This blurb is confusing, but I realized it means Grub can be used as the boot equipment for a CD in the same way we would use Syslinux with isohybrid MBR entries.

More compliant with UEFI-2.4 is to decide for either MBR or GPT and to append a copy of the EFI System Partition in order to avoid overlap of ISO partition and EFI partition. Here for MBR:

   -eltorito-alt-boot -e 'boot/grub/efi.img' -no-emul-boot \
   -append_partition 2 0xef ./CD_root/boot/grub/efi.img \

The resulting ISOs are supposed to boot from optical media and USB stick. One may omit option -eltorito-alt-boot if no option -b is used to make the ISO bootable via PC-BIOS.

That worked wonderfully for me to get a pure Grub result.

This is a super handy command for examining the commands that may have been used to generate an .iso file.

xorriso -hfsplus on -indev IMAGE.iso \
-report_el_torito plain -report_system_area plain \
-print "" -print "======= Proposal for xorrisofs options:" \
-report_el_torito as_mkisofs

Figuring out which modules are needed for what behavior is frustrating. There are docs, but they do not seem to easily correlate to “this is required to list devices with ls” and similar behavior. I eventually found the magic combo for grub-mkstandalone, which cuts down a few steps. Here’s how to do it with grub-mkimage. More verbose, but easier to debug.

Create a Grub early config file. Note that the label DEBIAN_CUSTOM matters. We must use the same label on our .iso so that Grub always knows where to find our boot media.

cat <<'EOF' >$HOME/LIVE_BOOT/scratch/embedded.cfg
search --no-floppy --set=root --label DEBIAN_CUSTOM
EOF

Generate a memdisk ourselves.

tar \
    -C $HOME/LIVE_BOOT/scratch \
    --transform 's;\.;boot/grub;' \
    -cf $HOME/LIVE_BOOT/scratch/memdisk.tar \
    ./grub.cfg

Bring it all together.

grub-mkimage \
    --directory=/usr/lib/grub/i386-pc \
    --format=i386-pc \
    --prefix="/boot/grub" \
    --output=$HOME/LIVE_BOOT/scratch/core.img \
    --memdisk=$HOME/LIVE_BOOT/scratch/memdisk.tar \
    --config=$HOME/LIVE_BOOT/scratch/embedded.cfg \
    linux normal iso9660 biosdisk memdisk search tar

Citations