Making rootfs
This weekend’s little project was creating a root file system image. The idea is to create an image that is bigger than what would be supported by the initrd.
Set-up
This project was performed from Alpine Linux.
apk add e2fsprogs
- The
e2fsprogspackage provides themkfs.ext4command for creating anext4file system.
If the host or container is not Alpine then download the statically linked
version of apk (Alpine Package Keeper) called apk.static and use/try that.
wget https://gitlab.alpinelinux.org/api/v4/projects/5/packages/generic/v2.14.10/x86_64/apk.static
chmod +x apk.static
Preparing root fs
The root file system will be prepared in a directory called root-source.
Using Alpine
apk --allow-untrusted -X https://dl-cdn.alpinelinux.org/alpine/latest-stable/main -U --allow-untrusted --root root-source --initdb add alpine-base
A problem with this approach at this time is the resulting image won’t be bootable. There is no boot loader installed and no provision for EFI either. This makes this only suitable for the direct boot option. Thus this requires a virtualisation platform where you can boot the kernel image directly. Firecracker, crosvm and QEMU can all do this.
I would like to come back to this another day as the main motivation for using initrd last time was to side-step this problem.
Using Wolfi from Chainguard
Spoiler: I didn’t end-up with a working system using this approach. This is likely to be expected as Wolfi is meant for containers only unlike Alpine.
For this case lets start with the initial problems encountered during the first few attempts at getting this working.
The first was Wolf lacks an init system so it fails. To overcome this the
following command was added ln -sf /usr/bin/busybox root-source/usr/bin/init
to instruct it to use busybox as the init system.
However, it failed with:
[ 5.063946] traps: sh[1] trap invalid opcode ip:723d6d5180d0 sp:7ffda96f5400 error:0 in ld-linux-x86-64.so.2[200d0,723d6d4f9000+29000]
[ 5.075462] Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000004
The issue here ended up being the same as a problem I experienced later on
when trying out bootc for the first time. The issue was QEMU was not configured
to allow the x86-64-v2 micro-architecture to be used. As such adding
-cpu max to the command line of qemu-system-x86_64 overcomes that problem.
Trying again resulted in a different error:
[ 5.089075] Run /sbin/init as init process
init: applet not found
[ 5.287827] Kernel panic - not syncing: Attempted to kill init! exitcode=0x00007f00
[ 5.293394] CPU: 0 UID: 0 PID: 1 Comm: init Not tainted 6.17.0-5-generic #5-Ubuntu PREEMPT(voluntary)
[ 5.297009] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.17.0-0-gb52ca86e094d-prebuilt.qemu.org 04/01/2014
This problem is that the init applet (command) isn’t enabled in Wolfi’s
version of busybox unlike Alpine’s. So the original approach of linking
busybox as init is not going to work.
Next up try OpenRC and sure enough that worked:
[ 4.634012] Run /sbin/init as init process
OpenRC init version 0.63 starting
Starting sysinit runlevel
OpenRC 0.63 is starting up Linux 6.17.0-5-generic (x86_64)
Next set of issues:
/etc/fstab does not exist/usr/lib/rc/sh/openrc-run.sh: line 64: hwclock: not found- This is include in the
util-linux-miscpackage.
- This is include in the
/usr/lib/rc/sh/openrc-run.sh: line 96: fsck: not found- This is include in the
util-linux-miscpackage.
- This is include in the
/usr/lib/rc/sh/openrc-run.sh: line 149: umount: not found** This is include in theumountpackage./usr/lib/rc/sh/openrc-run.sh: line 39: find: not found- This is included in the
findutilspackage.
- This is included in the
/usr/lib/rc/sh/openrc-run.sh: line 58: eend: not found/usr/lib/rc/sh/openrc-run.sh: line 211: checkpath: not found- This is included in the
openrcpackage as/usr/lib/rc/bin/checkpath
- This is included in the
/usr/lib/rc/sh/openrc-run.sh: line 156: grep: not found- This is included in the
greppackage.
- This is included in the
/usr/lib/rc/sh/openrc-run.sh: line 28: modprobe: not found- This is included in the
kmodpackage.
- This is included in the
I never did get this system properly booting.
apk --arch x86_64 -X https://apk.cgr.dev/chainguard/ -U --allow-untrusted --root root-source --timeout 60 --initdb add wolfi-base chainguard-keys mount umount openrc util-linux-misc findutils grep
ln -sf /sbin/openrc-init root-source/usr/bin/init
openrcfor the init system.util-linux-miscforhwclockandfsckused byopenrcmountfor mounting file-systems.- Several packages were needed for the
openrc-run.shscript, which were mentioned above.
Create the image
truncate -s 1G our.rootfs.ext4
mkfs.ext4 -d root-source -F our.rootfs.ext4
Testing with QEMU
curl --output ubuntu-netboot-kernel http://mirror.aarnet.edu.au/pub/ubuntu/releases/25.10/netboot/amd64/linux
qemu-system-x86_64 -m 512 -kernel ubuntu-netboot-kernel -drive if=virtio,format=raw,file=our.rootfs.ext4 -append "root=/dev/vda"
- On Windows add ` -accel whpx` to use the Windows Hypervisor Platform.
- The difference this makes is
[ 1.373455] Run /sbin/init as init processVerses[ 5.363164] Run /sbin/init as init process
- The difference this makes is
- The kernel used is from Ubuntu Netboot because the
linux-virtkernel by Alpine doesn’t include support for ext4.- It supports squashfs but not sure that can be provided via a block device (i.e. vda)
- This was discoverable by
ls /lib/modules/6.12.38-0-virt/kernels/fs - It had fat, fuse, nls, overlayfs and squashfs
Testing with Firecracker
Configuration
{
"boot-source": {
"kernel_image_path": "vmlinux",
"boot_args": "console=ttyS0 reboot=k panic=1 pci=off ip=10.200.0.2::10.200.0.1:255.255.255.255::eth0:off"
},
"drives": [
{
"drive_id": "rootfs",
"partuuid": null,
"is_root_device": true,
"cache_type": "Unsafe",
"is_read_only": false,
"path_on_host": "our.rootfs.ext4",
"io_engine": "Sync",
"rate_limiter": null,
"socket": null
}
],
"machine-config": {
"vcpu_count": 2,
"mem_size_mib": 2048,
"smt": false,
"track_dirty_pages": false,
"huge_pages": "None"
},
"cpu-config": null,
"balloon": null,
"network-interfaces": [
{
"iface_id": "net1",
"guest_mac": "06:00:AC:10:00:02",
"host_dev_name": "crosvm_tap"
}
],
"vsock": null,
"logger": null,
"metrics": null,
"mmds-config": null,
"entropy": null
}
Using 1024 MiB of memory wasn’t enough hand resulted in the error
Failure during vcpu run: Out of memory (os error 12)
Running
rm /tmp/fire-alpine.socket && firecracker --config-file alpine-rootfs.config --api-sock /tmp/fire-alpine.socket
Shutting down
curl --unix-socket /tmp/fire-alpine.socket -X PUT 'http://localhost/actions' -H 'Accept: application/json' -H 'Content-Type: application/json' -d '{ "action_type": "SendCtrlAltDel" }'
Problems
- Didn’t set-up root user.
chroot /rootfs /bin/sh -c 'adduser root; echo -e "root\nroot" | passwd root - Would probably be worth using the alpine-initrd /
mkramfs.shas a base such that it supports setting-up extra users and configuring ssh access.
Build from container
This was a little experiment I wanted to try.
For this we will try:
- valkey/valkey:8.1.4-alpine3.22 as the container
- umoci as the tool
- skopeo for downloading image - umoci doesn’t have built-iun support for getting an image
apk add umoci skopeo
skopeo copy docker://valkey/valkey:8.1.4-alpine3.22 oci:valkey:8.1.4-alpine3.22
umoci raw unpack --image valkey:8.1.4-alpine3.22 root-source
truncate -s 1G valkey.rootfs.ext4
mkfs.ext4 -d root-source -F valkey.rootfs.ext4
Problems
- No
/sbin/openrc - No suitable
/sbin/initwhich is configure to run the container.
Solutions
- Write a suitable init that uses the original container information. to start
- Create root-fs from Alpine with runc, convert image so its runnable with
runc an then set-up the init to use runc on start-up.
- Shame there is not a quadlet generator for OpenRC like there is for systemd.
- Don’t reinvent this and instead look to
bootc- however that is different approach then what I was thinking, as that is how to provision/create a host system using aContainerfile. I’m interested in taking an existing container and hosting it.
As such you could think of this as being either a second partition or second drive and image.
BootC
The bootc project is working the mission of Bootable Container Images.
In short, its about providing operating system updates using OCI container
images and reusing the tooling around containers for building images.
Features
- Immutable OS
- Readonly at runtime
- /etc
- /var
- Writable at build time.
- Supported are Fedora, CentOS Stream, RHEL
Experiment
I created a image using the “Bootable Container” extension for Podman Desktop. This means the commands used to turn the the httpd example container into an image aren’t provided here. I also missed the part where you can provide username and password.
To run the image with QEMU, it looked like this:
qemu-system-x86_64 -m 1G -drive if=virtio,format=raw,file=bootc-images\image\disk.raw -nographic -cpu max -net nic -net user,hostfwd=tcp::4080-:80
Confirm it works by visiting http://localhost:4080/
The -cpu max part was to ensure it supported x86-64-v2, as when running it
without it it would, fail on start-up, as seen below.
[ 1.792752] Run /init as init process
Fatal glibc error: CPU does not support x86-64-v2
[ 1.799499] Kernel panic - not syncing: Attempted to kill init! exitcode=0x00007f00
The -cpu max option was not compatible with the whpx accelerator, as it
would result in:
whpx: injection failed, MSI (0, 0) delivery: 0, dest_mode: 0, trigger mode: 0, vector: 0, lost (c0350005)
qemu-system-x86_64.exe: WHPX: Unexpected VP exit code 4
As a result the start time for the image was about 90 seconds which I believe attributed to not being able to use the visualisation acceleration option.
Overall neat, however my interest is not so much building a image for a new (virtual) machine from an OCI container image but for taking an existing OCI container image and baking it into a hard drive image and running it. Ideally, without the full container infrastructure (i.e. containerd, Podman, Docker).
I would be keen to revisit it for building Alpine images however it doesn’t
seem that is possible and from looking a little into it it seems the main thing
that bootc utilises is ostree and I’m not sure how compatible Alpine is with
that. Given my original project was to investigate building pre-built Alpine
images bootc for that does seem nice as it brings the familiarity of a
Containerfile to the process. I however find often the Containerfile format
to be limiting and thus you end up needing to resort to shell scripts anyway.
For more on bootc see the Creating bootc images from scratch in the
Red Hat Enterprise Linux documentation.
Future
Something else I would like to try is extracting the Alpine kernel from its
linux-lts package for direct-boot and to have support for ext4. However,
taking it to the next level would be building a kernel image for virtualisation
for use with QEMU, Firecracker and crosvm.
The bigger project is to continue with the idea of taking a container and running it. That is to drop the need to fetch the container after boot-time of the VM.
- Alpine + runc + squashfs of the extra OCI image
- Alpine + runc + custom container downloader.
- RedHat-based distribution (bootc) + runc + OCI image
I would like to checkout Compose FS and Torizon OS. The latter is intended to be fairly minimal and with a container runtime.
boot2container
This is a bonus section that I looked into before I published this, essentially I discovered a project which essentially does the kernel + initrd with podman and configure which container to pull and run via the kernel command line.
The project in question is boot2container.
curl -OL https://gitlab.freedesktop.org/gfx-ci/boot2container/-/releases/v0.9.17/downloads/initramfs.linux_amd64.cpio.xz
curl -OL https://gitlab.freedesktop.org/gfx-ci/boot2container/-/releases/v0.9.17/downloads/linux-x86_64
curl -OL https://gitlab.freedesktop.org/gfx-ci/boot2container/-/releases/v0.9.17/downloads/linux-x86_64-qemu
For Windows, the following runs QEMU using Windows Hypervisor Platform as the accelerator.
qemu-system-x86_64.exe -accel whpx -kernel linux-x86_64-qemu -initrd initramfs.linux_amd64.cpio.xz -nographic -m 384M -append "console=ttyS0 b2c.run=\"-ti docker.io/library/alpine:latest\""
Output
[ 3.869701] Run /init as init process
2025/10/13 10:11:29 Welcome to u-root!
...[snipped]
[10.38]: About to start executing a container
+ podman start -a 15599e00a88d86d4904aee13f0fd9be72c7c9f5ce2ff1a9e1876d0c40cf72d23
/ # ls
bin etc lib mnt proc run srv tmp var
dev home media opt root sbin sys usr
/ # cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.22.2
PRETTY_NAME="Alpine Linux v3.22"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"
/ #
boot2container with valkey
In this case the container is valkey, and the port it runs on 6379 is forwarded to the host.
qemu-system-x86_64.exe -accel whpx -kernel linux-x86_64-qemu -initrd initramfs.linux_amd64.cpio.xz -nographic -m 384M -net nic -net user,hostfwd=tcp::16379-:6379 -append "console=ttyS0 b2c.run=\"-ti docker.io/valkey/valkey:8.1.4-alpine3.22""
This means from Python can interact with it:
con = valkey.Valkey(host='localhost', port=16379, db=0)
con.set('foo', 'bar')
print(con.get('foo'))
A minor improvement to the QEMU command line can be done by adding:
-nodefaults -serial stdio which will disable the unused CDROM and video device.
boot2container verdict
This matches what I had in mind, in terms of initrd with podman. I would be
still keen to try initrd with runc or crun and the container pre-downloaded
on a secondary boot volume.
Not shown here is it supports adding a disk drive and telling it to automatically use it as a cache (it will format it as needed).
Conclusion
- I got too distracted by Wofli that I neglected fininshing up Alpine support.
- The
boot2containerproject looked very promising but I would sitll like to try my own spin on it if I get a chance.