๐Ÿš€ Powered by Raspberry Pi 5

klambert.net

A tiny computer, a custom operating system, and a website served straight from my home. This is the story of the Kevynx Pi project.

Live from the Pi

These numbers are read by PHP from the Raspberry Pi 5 every time you load this page.

Uptime
6d 23h 48m
Load (1 min)
0.00
Memory
0.9 / 7.8 GB
NVMe disk (233 GB)
7% used
Kernel
6.12.61-v8-16k
PHP
8.4.16

The project

Most people flash Raspberry Pi OS and call it a day. I wanted to understand every layer, so I built my own.

๐Ÿงฑ

Custom OS

The base system is Buildroot, a build system that produces a small, fast Linux tailored to the hardware. No bloat, only what I choose to include.

๐Ÿ“ฆ

Real package managers

Buildroot has no package manager by default, so I added opkg with the Entware and OpenWrt feeds. Now installing software is one command.

๐Ÿ”๏ธ

Alpine inside

A full Alpine Linux userland lives in a chroot. I get apk, a huge package catalog, and a desktop, without touching the base system.

๐ŸŒ

Self-hosted web

This site runs on nginx + PHP-FPM on the Pi and reaches the internet through a Cloudflare Tunnel, so no ports are opened on my router.

The stack

Everything running on this one little board.

Hardware

Raspberry Pi 5 (aarch64), 8 GB RAM, a 256 GB NVMe SSD on the PCIe bus as the only drive, USB mouse and keyboard, HDMI display.

Operating system

Buildroot 2026.08 with a 6.12 Linux kernel. BusyBox init, Dropbear SSH, and a tiny footprint: the whole root filesystem is a few hundred MB.

Web

nginx and PHP 8 FPM from Entware, running as an unprivileged user. Cloudflare Tunnel (cloudflared) brings in the traffic.

Desktop

XFCE on Xorg inside the Alpine chroot, drawn straight to the framebuffer on HDMI. One command, load_desktop, starts it.

BuildrootLinux 6.12aarch64opkg EntwareOpenWrt feedsAlpine 3.24apk XFCEXorgnginxPHP 8 PHP-FPMCloudflare TunnelDropbearNVMe

How a page gets to you

No port forwarding and no public IP. The Pi reaches out to Cloudflare, and visitors come in through that connection.

๐Ÿง‘โ€๐Ÿ’ป Your browserhttps://klambert.net
โ†“
โ˜๏ธ CloudflareDNS, TLS certificate, DDoS protection
โ†“
๐Ÿ”’ cloudflared tunnelan outbound-only encrypted connection from the Pi
โ†“
โšก nginxport 80, serves files and forwards PHP
โ†“
๐Ÿ˜ PHP-FPMruns index.php and builds this page, including the live stats

A look inside the chroot

Getting Alpine and a desktop running on a Buildroot system was the fun part. A chroot makes a folder look like a whole separate Linux install.

# mount the pseudo filesystems, then step inside Alpine
# mount -t proc proc /opt/alpine/proc
# mount -t sysfs sysfs /opt/alpine/sys
# mount -t devtmpfs devtmpfs /opt/alpine/dev
# chroot /opt/alpine /bin/sh -l

# now it's Alpine, with apk
/ # apk add nano htop curl git xfce4
/ # cat /etc/alpine-release
3.24.2

# back on the Buildroot side, one command opens the desktop
# load_desktop

Build log

How the system came together, step by step.

Step 1

Build the OS

Configured and compiled a Buildroot image for the Pi 5 and booted it from the SD card.

Step 2

Add package management

Set up opkg with the Entware and OpenWrt repositories, then fixed the feed and signature quirks until installs worked.

Step 3

Go live on the web

Installed nginx and PHP, then connected them to a Cloudflare Tunnel so the Pi is reachable from anywhere.

Step 4

Alpine chroot

Unpacked the Alpine minirootfs, mounted proc, sys and dev, and got apk installing packages.

Step 5

A real desktop

Installed XFCE and Xorg inside the chroot, drove the screen through the framebuffer, and fixed the mouse by running udev.

Step 6

Lock it down and automate

Switched SSH to key-only login, moved the web server to an unprivileged user, and made everything start on boot.

Step 7

Add the NVMe

Wiped the SSD, split it into a small FAT boot partition and an ext4 root, and copied the whole system over. The Pi now boots from the NVMe with no SD card at all.

Step 8

Make it feel like home

A custom login banner and colour prompts, an alpine command to jump into the chroot, text games from bsd-games, and a private Git repo to back up every config.

Things I learned

The problems that took the longest, and what fixed them.

  • A mouse needs udev. Xorg finds input devices through udev, so in a chroot you have to start it yourself or nothing responds.
  • No GPU driver, no problem. The kernel has no DRM device, so Xorg uses the plain framebuffer driver. It's slower, but it works.
  • Check the logs first. A mysterious 404 from nginx turned out to be PHP-FPM not running, with the error page missing on top.
  • Root is not a web user. PHP-FPM refuses to run as root by default for good reason. Dedicated users keep a compromised script away from the system.
  • The Pi has no clock battery. After a reboot the date was 1970, which broke the tunnel's TLS. The boot script now waits for DNS and sets the time first.
  • Boot order matters. The tunnel started before the network was ready, so a successful manual test still failed on the first real reboot.
  • Tools differ between systems. BusyBox has no pgrep and no sftp, so scripts have to use ps | grep and scp -O.

What's next

Plenty of ideas in the pipeline.

๐Ÿ’พ

Faster everything

The whole system now runs from the NVMe, so installs, builds and the desktop are much quicker than on the SD card.

๐Ÿ•น๏ธ

Retro games

Text classics like snake and klondike work today. Emulators are next.

๐Ÿ–ฅ๏ธ

Better resolution

The desktop is stuck at 640ร—480 for now. Time to tune the boot configuration.

๐Ÿ”Œ

IoT and hardware

GPIO projects, sensors and automation, with their data shown right here on this site.

๐Ÿ“

More write-ups

Detailed guides for each part of this build, so you can follow along on your own Pi.

๐Ÿ’ฝ

Backups

Automatic images of the SD card and the Alpine chroot, because every good system needs a way back.

๐Ÿงช

Code experiments

Small web apps and scripts, hosted and demoed on the Pi itself.