Your code is probably running in the cloud on a physical server. It may have a lot of software abstractions between your code and that bare metal server but there is no exception, code runs on a physical CPU.
In this post, we will discover the steps to boot a bare metal server and make it ready to run your code. It’s a long process which may seem complex at first. We will explain each step successively and illustrate what happens using snippets based on the code of OSS projects. Code will not make things simpler, but when a topic seems too obscure, I like to go really deep and observe there is no magic.
Booting a server is a complicated process, where multiple processes load and replace each other, with some touch of Assembly magic, but using a divide-and-conquer approach, we will discover that each step is understable in practice.
It’s a long post. I’ve worked with bare metal servers during the last years and this post condenses so many questions that I asked myself during those years.
- What is a system firmware.
- How works PXE and why iPXE is used in practice.
- What are the main files to boot a Linux kernel.
- How Systemd determines the processes to start.
I assume no prior knowledge on the topic. You only need to be interested in low-level details and know how to read C code (and some Assembly code). I ignored everything when I started working with bare metal servers in my job. I had to found out so many answers. Working on this post was the opportunity to validate my comprehension because it’s hard, if not impossible, to write (even to yourself) what you don’t understand.
The Really Short Story
A datacenter contains a lot of servers stacked in racks spreaded in different rooms. At first, the local disks on these servers are empty. Booting a Linux operating system on one of these fresh servers simply means executing the Linux kernel to start a few kernel threads before starting /sbin/init, with is usually Systemd, and which is responsible to start user processes. Except things couldn’t be that simple:
- The
/sbin/initis often present on the hard drive (when a server has already been installed) or must be downloaded (on a fresh installation without anything on the disks). In both cases, the kernel needs some drivers but as the kernel must fit in RAM, it has to be small and could not come with thousands of drivers to support every physical machine. - The kernel code must be available. You don’t want to install each server manually by attaching a media like a USB key. By using its network card, a server can ask at boot time “Who am I and what should I run?” It’s called a PXE boot (pronounced “pixie”) and could be used to download the kernel.
- PXE requires network drivers to download the kernel using various protocols (DHCP, TFTP, HTTP). UDP/DHCP is used as the server don’t have an IP address immediately. TFTP is used to download small files but is not practical for large files. iPXE implements the PXE standard but support additional protocols like HTTPS. But iPXE must be downloaded too, and executed.
- The server needs to know that it must boot on its network card. These preferences must be persisted when the server is turn off but cannot be written on disk for various reasons.
- PXE is implemented by the firmware of the network card (or by the firmware of the motherboard for integrated NIC). How to load a system firmware when the machine is power on. Firmwares are often implemented in C, which requires RAM, except one of the task of the firmware is to intialize the RAM. Assembly code is inevitable.
These are just a few challenges to have a running operating system. Most of them are solved by hacks and not necessarily by an elegant design. The boot process is a perillous journey, a succession of steps represented by different programs, each responsible for a subset of tasks, each responsible to load and execute the next one. The end result is an operating system running as if nothing has run before. Except the reality is completely different. Here is the step-by-step sequence of a standard PXE boot process for a Linux server:
| Step | Action | Component | Protocols/Files | OSS Projects |
|---|---|---|---|---|
| 1. System firmware Initializing | Performs hardware initialization, loads network drivers, and executes the UEFI Boot Manager to select network boot. | Motherboard Firmware | UEFI, UNDI, SNP | TianoCore EDK II, coreboot |
| 2. PXE Booting | Broadcasts a DHCP request to get an IP and the boot server details, then downloads and executes the UEFI bootloader. | NIC | DHCP, TFTP, bootx64.efi, grubx64.efi | iPXE, dnsmasq, ISC Kea |
| 3. Kernel Loading | The bootloader reads its configuration, fetches the Linux kernel and initial the initramfs into memory, which mounts the real root filesystem, and starts the /sbin/init system. | UEFI Bootloader | vmlinuz, initramfs, squashfs | Linux Kernel |
| 4. OS Initializing | Systemd or sysinit starts all required processes | - | - | Systemd, init |
And now begin the really, really, really long story… I don’t expect anyone to read the whole development, especially since we already know the end of the story. I wrote that blog post mostly to myself to identify the remaining gaps in my understanding, even after the countless questions I’ve asked myself over the past five years.
Step 1: System Firmware Initializing
When you turn on a server, the CPU cannot immediately run code. It needs to receive a stable electrical current but like when you are turning on a garden hose that has been sitting empty, during a few seconds, it spits out a mix of air and water. Therefore, the power supply waits for the electrical current to stabilize before sending a signal to the motherboard (called the Power Good signal). This signal tells the motherboard that the power is safe and it’s time to wake up the CPU. Waking up means sending electricity to specific pins on the processor. We are working with pure electronical circuits for now.
Modern CPUs are hardwired to read the first instruction at a specific memory address (known as the reset vector). For x86 CPUs, this address is 0xFFFFFFF0. Except the RAM is completely empty when a computer is turned off… There is nothing at that address. In practice, the CPU is not asking directly to the RAM. The motherboard chipset recognizes that the CPU asks for the Reset Vector address and redirect that request directly to the system firwmare (BIOS, UEFI).
Show me how that interception happens 👀 See the code
The firmware code is physically stored on the motherboard, completely separate from your main storage drives (SSD or HDD), on a small, non-volatile memory chip called an SPI Flash ROM.
- Non-volatile only means the chip retains its data even when the computer is shutdown.
- While ROM has been historically immutable (Read-Only Memory), modern chips can be rewritten electronically. This is what allows you to perform a “BIOS update” and flash a newer version of the firmware onto the chip.
If the firmware code is on the SPI Flash chip, the firmware configuration (your personal settings like the boot order) is stored in a separate, tiny amount of memory called NVRAM (or historically, CMOS). These settings are kept alive using a circular CR2032 coin-cell battery present on your motherboard. Remove the battery and you restore your firmware to its default settings.
In the late 1990s, BIOS was severely outdated. BIOS operates only in 16-bit processing mode (when x86 processors were supporting 32-bits mode). BIOS was slow and was unable to boot from hard drives larger than 2.2 TB.
Intel created EFI (Extensible Firmware Interface) as a modern replacement to overcome these limitations and introduce their high-end Itanium servers.
Intel also realized that to replace BIOS across the entire PC market, EFI must become an open standard. It handed the EFI specification over to an alliance of major tech companies (including Microsoft, Apple, AMD, Dell, and Lenovo). This group expanded EFI and renamed it UEFI (Unified Extensible Firmware Interface) in 2005.
UEFI is simply the modern, standardized evolution of EFI:
| Feature | EFI | UEFI |
|---|---|---|
| Ownership | Intel | The UEFI Forum |
| Era | Late 1990s to 2005 | 2005- |
| Scope | Intel servers | Universal (PCs, Macs, servers, and ARM devices) |
| Key Features | Lifted the 2.2 TB drive limit, improved graphical support | Added Secure Boot, network booting |
This article focuses exclusively on UEFI machines.
OSS EDK II https://www.github.com/tianocore/edk2- written in
- license SPDX
- since 2006–
To help EFI becomes UEFI, Intel released the code of their preferred EFI implementation. This served as the basis of the community-driven EDK II project, also known as TianoCore, reusing the original internal project name “Tiano” used by Intel. Although EDK II implements the UEFI specification and is the most popular solution for manufacturers to implement their firmwares, EDK II is not endorsed by the UEFI Forum.
Legacy BIOS was implemented in Assembly and wasn’t doing a lot. The main task was to check the hardware (CPU, RAM, motherboard) through a process called POST for Power-On Self-Test to ensure every component was physically present, receiving power, and functioning correctly. Any error and your server was beeping loudly.
UEFI firmwares are now usually implemented in C. It allows to do so much more. It’s like a mini-operating system that bootstraps the physical hardware, still doing a diagnostic, and proceeding in different steps.
1. SEC (Security Phase)
When the CPU wakes up, main RAM isn’t available yet. The CPU will use its own L1/L2/L3 cache as a temporary “RAM” (a technique called Cache-As-RAM) to set up a minimal environment to run minimal code. The SEC phase starts therefore in 100% Assembly, but it transitions to C as quickly as possible, even before RAM is available. Except that modern languages like C require a stack in memory to handle function calls, pass arguments, and store local variables. Assembly code is required to ensure we aren’t using the RAM at all.
Show me a simplifed version of that code 👀 See the code
2. PEI (Pre-EFI Initialization)
The main goal of this phase is to wake up the physical RAM.
- When PEI starts, the system is faking RAM using the CPU cache.
- When PEI finishes, the physical RAM is powered on and the execution has migrated to it.
Show me how the PEI phase accomplishes this 👀 See the code
RAM is now available so the CPU cache could do its normal job. PEI has completed its primary purpose.
And because physical memory is now vast, the system can afford to load more complex software. The PEI Core locates the compressed DXE (Driver Execution Environment) core in the motherboard ROM, decompresses it into physical RAM, and hands over execution.
Show me how the DXE core is called 👀 See the code
3. DXE (Driver Execution Environment)
This is the most elaborate phase, where UEFI truly acts like an OS. The DXE phase is like a Dependency Injection framework. Drivers don’t call each other directly. They publish interfaces (Protocols) that drivers could depend on.
Show me how drivers are loaded 👀 See the code
When the DXE phase completes, the physical hardware is awake, and many protocols have been discovered that allow the firmware to read hard drives, send network packets, and draw graphics on the screen. What you should really do?
4. BDS (Boot Device Selection)
The sole purpose of BDS is to use those freshly available protocols/APIs to find an operating system and hand over control.
The BDS phase doesn’t guess where the OS is. It reads instructions stored in the firmware configuration present in NVRAM, in particular, the variable BootOrder.
If the boot order is configured to start on the disks, BDS will use the devices initialized in the DXE phase. For a server starting in a datacenter, the boot order will usually be the network card, a process named PXE booting (Preboot eXecution Environment). Remind that booting on the network is the secret to control remotely a server running in a datacenter.
Show me how the bootloader emits network calls 👀 See the code
Step 2: PXE Booting
For that article, we consider our server has an integrated NIC. The code for the NIC is stored in the same memory chip where the system firmware resides. When pluging a physical network card, and when UEFI enters the DXE phase, it scans the PCIe bus and finds this NIC. UEFI will reach the NIC own physical ROM chip to load the driver as before. The logic is similar. The code is just present inside another ROM.
When a server is told to boot from the network, it goes through a strict sequence to find an operating system to load without touching a local hard drive at all. PXE is the industry standard.
1. DHCP
DHCP requests are sent over UDP (the server still don’t have an IP and it cannot therefore perform the three-way handshake required to establish a TCP connection). DHCP requests are binary. We cannot easily visualize the requests unlike HTTP requests but it’s enough to imagine a DHCP request as a succession of values:
- Source IP:
0.0.0.0(I don’t have an IP yet) - Destination IP:
255.255.255.255(Broadcast to everyone) - Source Port: UDP 68 (DHCP Client)
- Destination Port: UDP 67 (DHCP Server)
When the server tries to PXE boot, it sends a DHCP Discover packet. The following fields are required:
- OP (OpCode):
0x01(This is a Boot Request, not a Reply) - XID (Transaction ID):
0x39A8B12C(A random number so the client can match the server’s reply to its original request) - CHADDR (Hardware Address):
00:1A:2B:3C:4D:5E(The physical MAC address of the server’s network card) - Magic Cookie:
0x63825363(A specific hex value that proves this is a DHCP packet, not a legacy BOOTP packet)
In addition to fixed fields, DHCP supports optional fields. For a DHCP Discover request, the client includes several of these options to tell the server what it needs:
- Option 53 (Message Type):
1(= Discover). - Option 55 (Parameter Request List): The client lists the specific data it wants back from the server. For PXE, it will explicitly request Option
66(TFTP Server Name) and Option67(Bootfile Name). - Option 93 (Client System Architecture): Tells the DHCP server what kind of CPU the client has (e.g.,
0x0007for 64-bit UEFI,0x0000for Legacy BIOS). This allows the DHCP server to hand out the correct bootloader file.
The DHCP server replies with an IP address, plus requested option 66 (the IP of the boot server) and option 67 (the name of the bootloader file, like grubx64.efi).
A boot server is a machine on a network configured to hand out the IP addresses and the files necessary for other computers to start over the network. It is usually a combination of two key services:
- A DHCP Server: Useful to get an IP address and retrieve additional information using options as explained just above.
- A TFTP Server: A very simple file server that hosts the bootloader files.
The bootloader is responsible to load an actual operating system kernel (like Linux or Windows) into memory.
A bootloader file (also called a Network Bootstrap Program or NBP in PXE) is a tiny piece of software that the client computer downloads from the boot server into its RAM.
In practice, these bootloader files have an extension .efi and use the same file format used by Microsoft for Windows .exe. This format is known as PE/COFF (Portable Executable / Common Object File Format) binaries. When EFI was introduced, Intel partnered with Microsoft to transition away from Legacy BIOS. This file contains usually a compiled C program even if some developers has found how to use Rust or C++ to implement it.
dnsmasq https://www.github.com/simonkelley/dnsmasq- written in
- license GPL-2.0
- since 2000–
Created in 2000 by British software developer Simon Kelley, dnsmasq was born out of a practical need for efficiency. At the time, standard network tools like ISC BIND and ISC DHCP were simply too resource-heavy and complex to run on small home networks and low-power embedded systems. What started as a modest utility has since become an indispensable staple of modern networking.
Show me how to start a boot server 👀 See the code
It works. But this solution has many drawbacks:
- Standard PXE uses TFTP, which was designed in the 1980s:
- TFTP transfers files block-by-block and waits for an acknowledgment after every single block (important because UDP is used). On a local network, transferring a 50MB kernel might take a few seconds, but transferring a large ISO is just too long.
- TFTP doesn’t support encryption…
- Standard PXE relies on static configuration files. You return a static file to execute on the server. (no way to interact with the user, manage errors, retry, etc.)
Enter iPXE.
iPXE https://www.github.com/ipxe/ipxe- written in
- license GPL-2.0
- since 2010–
The roots of iPXE trace back to the Etherboot project (started in 1995) and its successor, gPXE. In 2010, frustrated by project management disputes and the slow integration of community patches, lead developer Michael Brown hard-forked the codebase to create iPXE. Unshackled from its predecessor’s constraints, iPXE rapidly evolved to support modern protocols like HTTP booting, iSCSI, FCoE, and Wi-Fi. Today, it has completely eclipsed gPXE to become the de facto open-source network boot firmware, quietly powering millions of bare metal server deployments in major cloud datacenters worldwide.
GRUB was designed to boot on local hard drives and was later adapted for the network. iPXE was built from the ground up entirely for the network. iPXE is more flexible while not necessarily harder to set up.
Show me how to start a boot server with iPXE 👀 See the code
Step 3: Kernel Loading
Before talking about the kernel, we need to talk about a few files first.
You are probably familiar with .iso files that you can download directly from the website of your favorite distribution.
An ISO file is just a digital replica of a CD. We can mount an ISO using a loop device just as if we had put a CD into a disk drive.
$ wget https://releases.ubuntu.com/resolute/ubuntu-26.04-live-server-amd64.iso$ sudo mkdir -p /mnt/iso$ sudo mount -o loop ubuntu-26.04-live-server-amd64.iso /mnt/iso$ cd /mnt/iso
$ ls -latotal 84dr-xr-xr-x 1 root root 2048 Apr 20 18:23 .drwxr-xr-x 3 root root 4096 Jul 28 13:10 ..dr-xr-xr-x 1 root root 2048 Apr 20 18:23 .diskdr-xr-xr-x 1 root root 2048 Apr 20 18:23 EFIdr-xr-xr-x 1 root root 2048 Apr 20 18:23 boot-r--r--r-- 1 root root 2048 Apr 20 18:23 boot.catalogdr-xr-xr-x 1 root root 4096 Apr 20 18:23 casperdr-xr-xr-x 1 root root 2048 Apr 20 18:20 dists-r--r--r-- 1 root root 63892 Apr 20 18:23 md5sum.txtdr-xr-xr-x 1 root root 2048 Apr 20 18:15 poollr-xr-xr-x 1 root root 1 Apr 20 18:15 ubuntu -> .
$ ls casper/initrdinstall-sources.yamlubuntu-server-minimal.squashfsvmlinuzThe files that will interest us are vmlinuz, initrd and squashfs. So what contains exactly these files?
vmlinuz = The Kernel$ sudo file vmlinuz-7.0.0-14-genericLinux kernel x86 boot executable, bzImage, version 7.0.0-14-generic(buildd@lcy02-amd64-043) #14-Ubuntu SMP PREEMPT_DYNAMIC Mon Apr 13 11:09:53 UTC 2026, RO-rootFS, Normal VGA, setup size 512*39, syssize 0x107420, jump 0x26c 0x8cd88ec0fc8cd239 instruction, protocol 2.15,from protected-mode code at offset 0x2cc 0x103a520 bytes ZST compressed, relocatable, handover offset 0x1066fc0,legacy 64-bit entry point, can be above 4G,32-bit EFI handoff entry point, 64-bit EFI handoff entry point,EFI kexec boot support, xloadflags bit 5, max cmdline size 2047, init_size 0x44c0000The file is a bzImage file, a file format specifically designed by the Linux kernel (Linux kernel x86 boot executable) identified by the HdrS signature that must be present at offset 0x0202:
$ hexdump -C -s 0x202 -n 4 /mnt/iso/casper/vmlinuz# -C: Canonical hex+ASCII display.# -s 0x202: Skip to offset 0x0202 (514 bytes).# -n 4: Read exactly 4 bytes.00000202 48 64 72 53 |HdrS|00000206This file is also a native UEFI application (32-bit EFI handoff entry point, 64-bit EFI handoff entry point). When the kernel is compiled, special headers MZ and PE are added to make the kernel looks like a standard PE/COFF executable (CONFIG_EFI=y CONFIG_EFI_STUB=y make bzImage).
The “MZ” signature, named after Microsoft engineer Mark Zbikowski is easy to locate because it’s present at the start of the file:
$ hexdump -C -n 2 vmlinuz# -C: Canonical hex+ASCII display.# -n 2: Read exactly 2 bytes.00000000 4d 5a |MZ|00000002The “PE” signature starts at the memory offset found by reading the 4 bytes at offset 0x3C:
$ hexdump -s 0x3c -n 4 -C vmlinuz0000003c 40 00 00 00 |@...|$ hexdump -s 0x40 -n 4 -C vmlinuz00000040 50 45 00 00 |PE..|vmlinuz is really a special file. The kernel is a large codebase with millions of lines of C source code that is too big. Space is limited at boot time. Here is the trick:
- Developers compile the kernel using
gccand aMakefile(often using a command likemake bzImage). - The output is a standard executable binary (an ELF file) called
vmlinux. (covered next) - This binary is compressed (using
gzip,bzip2,lzma, orzstd) and a tiny “uncompression stub” is glued to the front of it. The resulting file isvmlinuz(the “z” means zipped).
Because it has that uncompressed stub glued to the front, you cannot just use a standard command like unzip vmlinuz. You have to locate the compressed data inside the file. The easiest way is to use a script provided by the Linux kernel source called extract-vmlinux.
$ sudo apt-get install linux-headers-$(uname -r)# Download the script for your current kernel version$ sudo /usr/src/linux-headers-$(uname -r)/scripts/extract-vmlinux /mnt/iso/casper/vmlinuz > vmlinuxextract-vmlinux: Extracted vmlinux using 'unzstd' from offset 21197The command extract-vmlinux uses a brute-force approach. It uses standard command-line tools to scan the file byte-by-byte, looking for the “magic numbers” (hexadecimal file signatures) of common compression algorithms (ex: 1f 8b 08 for gzip). When it found that offset, it run a command like dd if=bzImage bs=1 skip=18432 | gunzip > vmlinux to extract the large kernel file.
In practice, at boot time, things are slighly different. When the file vmlinuz is build, a special header is present in the uncompressed part to quickly locate the compressed vmlinux. The raw code to decompress is also present in vmlinuz as no external binaries still exists.
To sum up, vmlinuz acts like a self-extracting archive that pretends to be a motherboard-level executable (PE/COFF or bzImage), while hiding the real vmlinux file deep inside its belly.
vmlinux$ sudo file vmlinuxvmlinux: ELF 64-bit LSB executableThe Linux kernel vmlinux is a program using the ELF format (Executable and Linkable Format), the standard file format used for executables on Linux (the equivalent of a .exe file on Windows). When you type a basic command like ls, grep, or python, you are running an ELF file too:
$ readelf -a vmlinuxELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x3f23520 Start of program headers: 64 (bytes into file) Start of section headers: 69206560 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 3 Size of section headers: 64 (bytes) Number of section headers: 42 Section header string table index: 41# ...The kernel is the “brain” of the Linux operating system that make accessible hardware resources to your processes and ensure every process get a chance to run. The kernel is kept intentionally small, meaning it doesn’t contain the thousands of drivers needed to mount every possible hard drive. The kernel must be loaded in RAM. It starts by understanding the physical world it woke up in. It probes the hardware, sets up memory management, configures the CPU, and starts the system timer. The kernel must then mount your computer hard drive to access the rest of the operating system files but it hits a classic chicken-and-egg problem:
- To mount the hard drive, the kernel needs the specific driver for your storage controller (like an NVMe, RAID, or SATA driver).
- But those drivers are stored on the hard drive it is trying to mount…
To solve this, the bootloader (before it died) actually loaded a second file into RAM alongside the kernel.
initrd or initramfs (Initial RAM Filesystem)The name initrd is used by some Linux distributions and initramfs by others. Both represent the same thing.
$ file /mnt/iso/casper/initrd/mnt/iso/casper/initrd: ASCII cpio archive (SVR4 with no CRC)It’s small. It contains just enough drivers and modules for the kernel to recognize your physical hard drive. You can inspect its content using the command unmkinitramfs
$ unmkinitramfs /mnt/iso/casper/initrd my_initrd/$ cd my_initrd/$ lsbin conf etc init kernel lib lib64 run sbin scripts usr var$ du -csh .148M .The goal is to mount the target file system (often on the hard drive). The initramfs is just a step towards that goal.
How to build the most minimal initramfs?
- Many people build a custom
initramfsfor their specific hardware. When you install manually a Linux distribution (like Ubuntu, Fedora, or Arch) or update your kernel, a background script automatically generates aninitramfslocally on your machine. Tools likedracutormkinitcpioinspect your current system (your storage controllers, your file systems) and decide which kernel modules to include. - In some situations, a generic
initramfsis used like when booting on a Live USB (Linux distributions also come with a defaultinitramfs). To support many different hardware configurations without bloating the file, Linux relies on some optimization tricks:- Ignore audio, GPU and more network drivers,
- Use BusyBox to bundle common UNIX utilities into a single executable file,
- Use efficient compression algorithms like Zstandard or XZ.
We know that to load the kernel, we need a file vmlinuz containing the code inside vmlinux and a file initramfs containing the modules to load the target file system. What does it really mean to load a kernel in practice?
We have seen that the file vmlinuz presents itself as a traditional bzImage and as a UEFI application. Historically, GRUB (or iXPE) was required to load the kernel, but as the kernel now behaves like a UEFI application, GRUB is optional as the system firmware knows how to do it.
In our case, we are using iPXE to download and boot the kernel. iPXE has already replaced the system firmware in RAM and is thus responsible to load the kernel itself.
Show me how iPXE is loading the kernel 👀 See the code
The kernel is now running. What does it do at boot time?
When iPXE hands off control to the kernel, the system first runs architecture-specific assembly code to set up a basic environment. The goal is to unpack vmlinux using low-level code that we’ll not cover. Once that environment is ready, it jumps to start_kernel() defined in file init/main.c. This is the most important file where all initialization functions happen before launching the first user-space init process.
Show me the steps done by the kernel at boot time 👀 See the code
The kernel completes by running the script /init present in initramfs. Before we continue, we still need to talk about a new file.
squashfs = The Root FilesystemImagine your server is not booting correctly on the operating system installed on its hard drive. How to debug? Using a squashfs file is a solution.
$ file ubuntu-server-minimal.squashfsubuntu-server-minimal.squashfs: Squashfs filesystem,A squashfs is a highly compressed, read-only file system. If you look inside an Ubuntu ISO, the squashfs file is usually the largest file there (often 2GB+). The squashfs contains the entire directory structure of the operating system (/bin, /etc, /usr, /var).
A squashfs file is built using a tool called mksquashfs.
- The OS developers create a massive folder containing a fully working Linux installation (with the
/bin,/etc, and/usrdirectories, desktop environments, etc.). - They run
mksquashfs /path/to/folder filesystem.squashfs. - The tool heavily compresses the folder into a single, read-only block device image.
Because a squashfs file is literally a filesystem, you can mount the file using a loop device:
$ sudo mkdir /mnt/squash$ sudo mount -t squashfs -o loop ubuntu-server-minimal.squashfs /mnt/squashInstead of iPXE downloading the squashfs into memory, iPXE usually downloads the kernel and the initramfs. It then uses the kernel command line (imgargs) to pass a URL to the squashfs like this:
#!ipxe# 1. Load the kernel and initramfskernel http://192.168.1.10/vmlinuzinitrd http://192.168.1.10/initrd.img
# 2. Append the command line arguments# 'fetch=' tells the initramfs where to go get the squashfsimgargs vmlinuz initrd=initrd.img boot=casper fetch=http://192.168.1.10/filesystem.squashfs
# 3. Boot the systembootWhen the kernel executes the file /init inside initramfs on a fresh server, the environment is completely empty as we still have nothing on the hard drive. This script (written in Bash) will load various kernel modules, mount the squashfs file and seamlessly transition to it so that it becomes the new / before calling /sbin/bin.
Show me how /init works 👀 See the code
Step 4: OS Initializing
In modern Linux systems, /sbin/init is almost always a symlink to /lib/systemd/systemd.
systemd https://www.github.com/systemd/systemd- written in
- license LGPL-2.1
- since 2010–
In 2010, software engineers Lennart Poettering and Kay Sievers set out to solve a deeply ingrained frustration with Linux boot times and service management. The traditional SysV init system was notoriously slow, relying on a sequential execution of shell scripts that created massive bottlenecks during startup. Inspired by the elegant efficiency of Apple’s launchd, they envisioned a modern, dependency-based replacement that could drastically speed up booting by aggressively parallelizing service startups through socket and D-Bus activation. Systemd is now used by nearly all major Linux distributions.
Systemd starts by building a dependency graph to determine what needs to start, and in what order:
- Finding the default target: It looks for a specific unit file called
default.target(located in/etc/systemd/system/). This is usually a symlink to eithermulti-user.target(command line) orgraphical.target(GUI). - Pulling dependencies: Systemd reads this target file and looks for directives like
Wants=andRequires=. Ifgraphical.targetwantsdisplay-manager.service, Systemd adds the display manager to the queue. - Determining the order: To know which processes can start in parallel and which must wait, it evaluates
After=andBefore=directives. If a web server hasAfter=network.target, Systemd guarantees the network is up before launching the web server. - Starting the transaction: It compiles all these units into a “transaction”. If there are conflicting dependencies, the transaction fails and an emergency shell is displayed.
For example, on a server running Ubuntu 26.04 Desktop:
$ ls -l /lib/systemd/system/default.targetlrwxrwxrwx 1 root root 16 Aug 3 12:44 /lib/systemd/system/default.target -> graphical.target
$ cat /lib/systemd/system/graphical.target[Unit]Description=Graphical InterfaceDocumentation=man:systemd.special(7)Requires=multi-user.targetWants=display-manager.serviceConflicts=rescue.service rescue.targetAfter=multi-user.target rescue.service rescue.target display-manager.serviceAllowIsolate=yesIf your code is running as a Systemd unit, it means that you have created a service unit file like myprogram.service under /etc/systemd/system/.
[Unit]Description=My Custom Program ServiceAfter=network.target
[Service]ExecStart=/usr/local/bin/myprogramRestart=on-failureUser=ubuntu
[Install]WantedBy=multi-user.targetThe unit must be enabled using sudo systemctl enable myprogram.service to start and WantedBy must be referenced in the dependency graph built by Systemd. In this case, we declared multi-user.target, which is present in After in the default graphical.target.
If your code is running as a container on Kubernetes, other units like kubelet.service and containerd.service have been configured. Kubernetes will start and ensure your container is up and running.
Show me how Systemd starts 👀 See the code
That’s it! We have covered all the steps between a server being powered on in a datacenter until your code finally get executed.