Frood, an Alpine Initramfs NAS (2024)
Frood: An Alpine Initramfs NAS (2024)
My NAS, known as frood, utilizes a somewhat unconventional architecture. Rather than a traditional installation, it consists of one massive initramfs that houses the entire Alpine Linux system.
As long as the system firmware can locate the image, the machine boots seamlessly. The entire environment is defined declaratively within the git repository used to build the initramfs.
The Philosophy of Simplicity
One of the most important aspects of this setup is the lack of a complex Domain Specific Language (DSL). The logic is straightforward:
- If I need a file at
/etc/example.conf, I simply place it inroot/etc/example.conf. - The heavy lifting is handled by a few hundred lines of scripts that are fully readable.
- Configuration feels identical to managing a standard Alpine system.
Note: For those already sold on the idea, feel free to jump straight to the "How it works" section.
Evolution and Motivation
I have recently updated the setup for 2025, migrating to a 64-core Ampere Altra processor and replacing the traditional bootloader with a single-file UKI (Unified Kernel Image).
Why run from memory?
Running a system entirely from RAM offers two primary advantages:
- Speed: Execution is incredibly fast.
- Hardware Longevity: It eliminates wear and tear on system storage (often unreliable SD cards), allowing high-quality drives to be dedicated solely to the ZFS pool.
The Persistence Problem
The challenge with RAM-based systems is persisting configuration changes. Alpine typically solves this via "diskless mode", which uses an overlay file.
Comparison of Persistence Methods:
| Feature | Standard Alpine Diskless | Frood (Initramfs) |
|---|---|---|
| Mechanism | .apkovl overlay files | Declarative Git Repo |
| Management | lbu(1) tool | Git commit Rebuild |
| Complexity | High (many moving parts) | Low (files in folders) |
| Reliability | Prone to mount/apk failures | Atomic and immutable |
The standard lbu process is complex: it must find the overlay, apply it, mount filesystems via fstab, and install missing apk packages. In the past year, this broke multiple times for me.
"Erase Your Darlings"
I strongly agree with Graham Christensen's argument for immutable systems. System state often hides in /etc or /var, representing undocumented "quick fixes" that are forgotten over time.
- "Right, just run
myapp-initonce." - "Just download these ca-certificates to fix that one error."
- "Touch
/etc/ipsec.secretsor the tunnel fails."
These "oh, oops" moments are the ghosts that haunt future upgrades (like the dreaded RHEL 7 to 8 transition). I previously tried using Ansible, but that created a redundant layer: Ansible Deploy lbu apkovl.
While other declarative options exist, I found them lacking:
NixOS(Doesn't sound fun)gokrazy(Not ready for ZFS)buildrootoru-root(Embedded toolchains)
Ultimately, I prefer Alpine: it is lightweight, GNU-less, and simple.
How it Works
During the boot process, the Linux kernel expects an initramfs image—a cpio archive acting as the initial root filesystem. While usually used to load modules and pivot to a real rootfs, there is nothing stopping us from putting the entire OS inside it.
The Build Process
The foundation is alpine-make-rootfs, a script of roughly 500 lines designed for container images.
Build Requirements:
-
alpine-make-rootfsscript -
packagesdefinition file -
setup.shconfiguration script -
root/directory for skeletal files
The Build Script:
#!/bin/sh
set -e
wget https://raw.githubusercontent.com/alpinelinux/alpine-make-rootfs/v0.7.0/alpine-make-rootfs \
echo '91ceb95b020260832417b01e45ce02c3a250c4527835d1bdf486bf44f80287dc alpine-make-rootfs' | sha256sum -c \
chmod +x alpine-make-rootfs
ROOTFS_DEST=$(mktemp -d)
# Prevent mkinitfs from running during apk install
mkdir -p $ROOTFS_DEST/etc/mkinitfs
echo disable_trigger=yes $ROOTFS_DEST/etc/mkinitfs/mkinitfs.conf
export ALPINE_BRANCH=edge
export SCRIPT_CHROOT=yes
export FS_SKEL_DIR=root
export FS_SKEL_CHOWN=root:root
PACKAGES=$(cat packages)
export PACKAGES
./alpine-make-rootfs $ROOTFS_DEST setup.sh
To finalize the image, we exclude the /boot directory (which contains the kernel) and package the rest:
-path ./boot -prune -o -print | cpio -o -H newc | gzip $ROOTFS_DEST/boot/initramfs-lts
The mathematical logic of the boot sequence can be simplified as:
Packages and Configuration
I use standard server packages. alpine-base provides the essentials: apk, busybox, and openrc. To save space, I use linux-firmware-none (which provides linux-firmware-any) to avoid downloading hundreds of megabytes of unnecessary drivers.
The setup.sh script (Declarative Service Management):
#!/bin/sh
set -e
# System Initialization
rc-update add devfs sysinit
rc-update add dmesg sysinit
rc-update add hwclock boot
rc-update add modules boot
rc-update add sysctl boot
rc-update add hostname boot
rc-update add bootmisc boot
rc-update add syslog boot
rc-update add klogd boot
rc-update add networking boot
rc-update add seedrng boot
# Shutdown/Default Services
rc-update add mount-ro shutdown
rc-update add killprocs shutdown
rc-update add acpid default
rc-update add crond default
rc-update add local default
rc-update add openntpd default
rc-update add sshd default
rc-update add tailscale default
# Set root password
chpasswd -e 'EOF'
root:$6$twsDxnP.TG2M8J4l$7lte7E/ImK4UwoursD7qQCC7XMUothIDb9FTH1MncxYbGQDUQPkC/9pxleTwPxEs3nbatApszxuwc4yj6ucdX1
EOF
Visuals and Context

"Yeah I think all my objections to Alpine are basically its flaky init and its persistency mechanism... if I run apk at build time to make a chonky initramfs, write 300 lines to replace init, I might be golden... all of the mkinitfs complexity and flakyness is in finding the modules, loading them, finding the root, finding the apk cache, installing it... all of that goes poof."

