Linux Boot Process
0/0 checks
BIOS/UEFI to Userspace
graph TD
classDef fw fill:#2c3e50,stroke:#1a252f,color:#fff
classDef boot fill:#e67e22,stroke:#d35400,color:#fff
classDef kern fill:#e74c3c,stroke:#c0392b,color:#fff
classDef init fill:#9b59b6,stroke:#8e44ad,color:#fff
classDef svc fill:#2ecc71,stroke:#27ae60,color:#fff
POWER["Power ON"]:::fw
subgraph Firmware["Firmware (BIOS or UEFI)"]
POST["POST: Power-On Self Test check RAM, CPU, devices"]:::fw
BIOS["BIOS: reads MBR (first 512 bytes of disk) hands off to bootloader"]:::fw
UEFI["UEFI: reads EFI System Partition directly loads bootloader faster, Secure Boot support"]:::fw
end
GRUB["GRUB2 Bootloader /boot/grub2/grub.cfg shows OS selection menu loads kernel + initramfs into RAM"]:::boot
KERNEL["Linux Kernel decompresses itself initializes CPU, memory, devices mounts initramfs as temporary root /"]:::kern
INITRAMFS["initramfs (initial RAM filesystem) minimal filesystem in RAM contains drivers needed to mount real root (disk drivers, LVM, LUKS decrypt)"]:::kern
REALROOT["Mount real root filesystem (ext4/xfs on /dev/sda1 or LVM volume) switch_root to real /"]:::kern
INIT["PID 1: systemd (or SysVinit/OpenRC) first userspace process starts all services"]:::init
TARGETS["systemd targets multi-user.target graphical.target"]:::init
SERVICES["Services start in parallel sshd, networkd, nginx, docker, etc."]:::svc
LOGIN["Login prompt / getty"]:::svc
POWER --> Firmware
Firmware --> GRUB
GRUB --> KERNEL
KERNEL --> INITRAMFS
INITRAMFS --> REALROOT
REALROOT --> INIT
INIT --> TARGETS
TARGETS --> SERVICES
SERVICES --> LOGIN
The same sequence, one stage at a time:
1. Power ON → POST. Power-On Self Test checks RAM, CPU, and devices before anything disk-related happens.
2. Firmware hands off. BIOS reads the MBR (first 512 bytes of disk) and hands off to the bootloader; UEFI reads the EFI System Partition directly instead — faster, and with Secure Boot support.
3. GRUB2 bootloader. Reads
/boot/grub2/grub.cfg, shows the OS selection menu, and loads the kernel + initramfs into RAM.
4. Kernel starts. Decompresses itself, initializes CPU, memory, and devices, then mounts initramfs as a temporary root
/ — it has no drivers for the real disk yet.
5. initramfs takes over. A minimal RAM filesystem — busybox, dracut scripts, disk drivers as kernel modules,
cryptsetup — carries just enough to reach the real root: load disk drivers, decrypt LUKS, assemble LVM/RAID.
6. Real root mounted. The real filesystem (ext4/xfs on
/dev/sda1 or an LVM volume) is mounted, switch_root runs, and initramfs is discarded from RAM.
7. PID 1: systemd. The first userspace process starts. Everything before this point ran in kernel space or a throwaway RAM filesystem — this is where "the system" actually begins.
8. Targets resolve. systemd walks the dependency chain down from
default.target (usually multi-user or graphical) through basic.target to sysinit.target, mounting filesystems, swap, and setting the hostname along the way.
9. Services start in parallel.
sshd, networkd, nginx, docker, and anything else wanted by the active target — unlike SysVinit's serial startup, systemd starts what it can at once and lets After=/Requires= ordering handle the rest.
10. Login prompt.
getty hands control to a human (or the display manager, for a graphical target).
BIOS vs UEFI:
| BIOS | UEFI | |
|---|---|---|
| Partition table | MBR (max 2TB disk, 4 partitions) | GPT (max 9.4ZB, 128 partitions) |
| Boot code location | First 512 bytes of disk (MBR) | EFI System Partition (FAT32, /boot/efi) |
| Secure Boot | No | Yes (verifies bootloader signature) |
| Speed | Slower | Faster (parallel init) |
| Common on | Pre-2012 hardware | All modern hardware |
A GRUB2 bootloader loads two things into RAM before handing off to the kernel. What are they?
The kernel itself, and the initramfs image. GRUB doesn't hand off to a bare kernel — it loads both together, so the kernel has an initramfs ready to mount as its temporary root the moment it initializes.
initramfs and Why It Exists
The kernel can't mount the real root filesystem directly because it might need drivers that aren't compiled in (e.g., LUKS encryption, LVM, NFS root, specific disk drivers). initramfs is a temporary filesystem that provides those drivers.
graph LR
classDef kern fill:#e74c3c,stroke:#c0392b,color:#fff
classDef init fill:#9b59b6,stroke:#8e44ad,color:#fff
classDef real fill:#2ecc71,stroke:#27ae60,color:#fff
KERNEL["Kernel loaded into RAM no disk drivers yet"]:::kern
INITRD["initramfs mounted as / has: busybox, dracut scripts disk drivers as kernel modules cryptsetup for LUKS"]:::init
DRIVERS["Load disk drivers decrypt LUKS volume assemble LVM/RAID"]:::init
REAL["Mount real / (ext4/xfs) switch_root initramfs discarded from RAM"]:::real
KERNEL --> INITRD --> DRIVERS --> REAL
# Inspect initramfs contents
lsinitrd /boot/initramfs-$(uname -r).img
# Rebuild after driver changes
dracut --force # RHEL/CentOS
update-initramfs -u # Debian/Ubuntu
Why can't the kernel just mount the real root filesystem directly, instead of mounting initramfs first?
It might not have the drivers it needs compiled in — LUKS decryption, LVM assembly, NFS root, or the specific disk controller driver. initramfs carries a minimal set of tools and modules (busybox, dracut scripts, disk drivers,
cryptsetup) just to bridge that gap: load the right drivers, decrypt/assemble what's needed, then switch_root onto the real filesystem and discard itself from RAM.systemd
systemd is PID 1 — the init system that starts and manages all services. It replaced SysVinit for parallel service startup and better dependency management.
graph TD
classDef target fill:#9b59b6,stroke:#8e44ad,color:#fff
classDef svc fill:#2ecc71,stroke:#27ae60,color:#fff
classDef dep fill:#3498db,stroke:#2980b9,color:#fff
DEFAULT["default.target (usually multi-user or graphical)"]:::target
DEFAULT --> MULTI["multi-user.target all non-GUI services"]:::target
MULTI --> BASIC["basic.target timers, paths, sockets"]:::target
BASIC --> SYSINIT["sysinit.target mount filesystems, swap, hostname"]:::target
MULTI --> SSHD["sshd.service"]:::svc
MULTI --> NGINX["nginx.service"]:::svc
MULTI --> DOCKER["docker.service"]:::svc
MULTI --> NETWORK["network-online.target"]:::dep
NETWORK --> APP["myapp.service After=network-online.target"]:::svc
Unit File Anatomy
# /etc/systemd/system/myapp.service
[Unit]
Description=My Go Application
After=network-online.target postgresql.service # start after these
Requires=postgresql.service # hard dependency (fail if postgres fails)
Wants=redis.service # soft dependency (start redis if possible)
[Service]
Type=simple # exec: ExecStart is the main process
ExecStart=/usr/local/bin/myapp --config /etc/myapp/config.yaml
ExecReload=/bin/kill -HUP $MAINPID # reload config on: systemctl reload myapp
WorkingDirectory=/opt/myapp
User=appuser
Group=appuser
Restart=on-failure # restart if exits non-zero
RestartSec=5s # wait 5s before restart
StartLimitBurst=5 # max 5 restarts
StartLimitIntervalSec=30s # within 30s window — then fail permanently
# Resource limits
LimitNOFILE=65536 # max open file descriptors
MemoryMax=512M # cgroup memory limit
CPUQuota=200% # max 2 CPU cores
# Security hardening
NoNewPrivileges=true # process cannot gain extra privileges
PrivateTmp=true # own /tmp (not shared with other services)
ProtectSystem=strict # /usr, /boot, /etc read-only
ReadWritePaths=/var/lib/myapp # only this path is writable
# Environment
Environment=APP_ENV=production
EnvironmentFile=/etc/myapp/env # load from file
[Install]
WantedBy=multi-user.target # enable for this target
Essential systemctl Commands
systemctl start myapp
systemctl stop myapp
systemctl restart myapp
systemctl reload myapp # reload config (sends ExecReload signal)
systemctl status myapp # detailed status + last log lines
systemctl enable myapp # auto-start on boot
systemctl disable myapp
systemctl daemon-reload # reload unit files after editing
systemctl list-units --failed # see broken services
systemctl list-dependencies myapp # show dependency tree
# Logs
journalctl -u myapp -f # follow logs
journalctl -u myapp --since "1 hour ago"
journalctl -u myapp -n 100 # last 100 lines
journalctl -p err -b # errors since last boot
journalctl --disk-usage # how much journal space used
# Boot analysis
systemd-analyze # total boot time
systemd-analyze blame # which units took longest
systemd-analyze critical-chain # critical path of boot
Service Types
| Type | Meaning | Use for |
|---|---|---|
simple |
ExecStart process IS the service | Most daemons |
forking |
ExecStart forks, parent exits | Old-style daemons (nginx, apache) |
notify |
Process sends sd_notify(READY=1) when ready |
Services that take time to init |
oneshot |
Process runs and exits (not a daemon) | Init scripts, migrations |
idle |
Like simple but waits until boot is done | Low-priority startup tasks |
In a systemd unit file, what's the actual difference between Requires= and Wants=?
Requires= is a hard dependency — if that unit fails, this one fails too. Wants= is a soft dependency — systemd tries to start the listed unit alongside this one, but if it fails, this unit still starts anyway. Neither one controls ordering by itself — that's what After=/Before= are for, which is why unit files often pair Requires= or Wants= with an After= line naming the same unit.