Making KioskPi — custom Raspberry Pi OS image using pi-gen

How to make use of Pi-Gen, a tool used to create Raspberry Pi OS images. Today, we’re gonna create our own Pi OS image featuring a Web-Based Kiosk with HDMI-CEC control. Minimalistic and performance-oriented. No desktop environments or Chromes to slow us down.

My specific use case is making a digital menu card to be displayed at three portrait mounted TVs in my friend’s canteen.

Raspberry Pi

TLDR: See ready to build project on GitHub.

Setting up the tool

As per pi-gen’s readme, we’ll use Ubuntu or other Debian-based system. And install some dependencies.

sudo apt install coreutils quilt parted qemu-user-static debootstrap zerofree zip dosfstools libarchive-tools libcap2-bin grep rsync xz-utils file git curl bc qemu-utils kpartx gpg pigz binfmt-support

(note that binfmt-support isn’t listed in the readme, but I had it missing)

Now clone the pi-gen tool.

git clone --depth 1


First things first, we’ll need to create a file named config
Here’s how mine works.

FIRST_USER_PASS="..." # your password
PUBKEY_SSH_FIRST_USER="ssh-rsa ..." # your public key for SSH
WPA_ESSID="YourWifi" # your wifi - if needed
WPA_PASSWORD="..." # your wifi password
STAGE_LIST="stage0 stage1 stage2 stage2-kiosk"
KIOSK_URL="" # Kiosk's predefined URL

It’s pretty self-explanatory, if in doubt, refer to pi-gen readme manual.

About stages

„Build is divided up into several stages for logical clarity and modularity.”
— the manual

List of stages, simplified for our purposes, would look like this:

  • Stage 0: Bootstrap
  • Stage 1: Truly Minimal System
  • Stage 2: Lite System
  • Stage 3: Desktop System
  • Stage 4: Normal Raspberry Pi OS image
  • Stage 5: The Raspberry Pi OS Full image

For more detailed description, refer to the manual.

So where to stop? Our Kiosk doesn’t really require a desktop environment, just a plain X server. Therefore, we’ll stop after Stage 2 and make our own ending stage. Notice stage2-kiosk in STAGE_LIST array of our config file.

If you want your system a little lighter, I suggest carefully trimming packages from stage2/01-sys-tweaks

Custom Build Stage

This is where we’ll make most of our customisations. Let’s start with using my template from GitHub.

Clone this stage2-kiosk directory into your pi-gen's root, next to other stages. Also, don’t forget to stop exporting at (basic) Stage 2.

touch stage2/SKIP_IMAGES stage2/SKIP_NOOBS

Let’s have a look at some interesting files in our stage2-kiosk/

EXPORT_IMAGE — an empty file that says to export our custom image at the end of this stage

00-kiosk/00-packages — a list of dependencies, notably

  • xorg and some utils, a plain X server with no desktop environment
  • gjs & gir1.2-webkit2–4.0 — Gnome JavaScript binder & WebKit library for our minimalist web browser (see below)
  • cec-utils & xdotool for our HDMI-CEC to key strokes interpreter (see below)

00-kiosk/ — installer script to put our 00-kiosk/files in order.

Let’s dig into files

Kiosk’s starting logic would be this: Automatic login would trigger X and open browser with predefined URL in fullscreen.

Automatic Login → .profile → startx → .xinitrc → custom browser

First, let’s look at our script. It’s purpose is to set up a read-only Overlay FS, as explained in Best Practices by the end of this article.

echo ">> Enabling Read-Only Overlay File System"
sudo raspi-config nonint enable_overlayfs
sudo raspi-config nonint enable_bootro
# remove self
rm ./
echo ">> Rebooting"
sudo reboot

Our .profile should contain it’s default content, but it should also run startx on the first console

...previous content
...some of my favourite aliases

# first-run script
[[ -f ./ ]] && ./
# silent startx on video console[[ -z $DISPLAY && $XDG_VTNR -eq 1 ]] && startx > /dev/null 2>&1

The .xinitrc, on the other hand, is a one-liner


It’s the where all the magic is done

#!/usr/bin/env bash

# read URL from easily accessible location
URL=$(head -n 1 /boot/kiosk.url)

# never blank the screen
xset s off -dpms

# rotate to portrait mounted TV
xrandr --output HDMI-1 --rotate left

# show a splash before browser kicks in
feh --bg-scale splash.png

# start CEC Listener & Browser concurrently
(cec-client | cec2kbd) & \
browser --fullscreen "${URL:=''}"

Pretty straightforward, huh? :-) But 2 main items remain to explore.

The Browser

I’ve used Andrea Giammarchi’s brilliant little browser script written in JavaScript for GTK 👏

There are no tabs, no settings, just a simple, fullscreen WebKit viewport.
No Chrome to eat our RAM.

I also highly recommend his article A Minimalistic 64 bit Web Kiosk for RPi 3 from 2018, which has been a great source of information and inspiration for my project & this article.

Fullscreen Web App showing options of Soups and Salads in Czech language and currency

Predefined URL is stored in /boot folder for convenience. It’s on a FAT filesystem and easy to edit even under Windows environment.


For purpose of my project, the user should be able to switch sites (sections of the canteen’s menu) on the screen easily. Since this would be run on TVs, they have remotes.

There’s a protocol called HDMI-CEC (Consumer Electronics Control) which passes remote’s commands to HDMI-connected devices.

That’s how I made this simple bash script to read output from cec-client and pass it on to the browser as key strokes using xdotool

I’ve only covered arrow keys and enter (or select), feel free to add more :-)

Remote’s keys sent as key strokes via HDMI-CEC

The target web app is listening to key strokes, swiping thru menu pages on arrows and reloading on Enter.

Let’s Build

sudo ./

Go get for some coffee ☕
After some time, you’ll see in your deploy folder.

Let’s Flash

Now go flash your SD card with Raspberry Pi Imager. It’ll also allow you to edit some configs, keys, passwords, wifies in its UI.

Then You’re Good to Go! 🎉
My final image has about 2.8 GB, or about 800 MB of ZIP. Not bad.

Best Practices

1) Easy URL editing

The address to load is saved under /boot/kiosk.url file. On this FAT type partition, you may simply edit this file under any environment.

2) Read-Only File System

For your convenience, read-only FS, or Overlay File System is set up after first boot, in order to protect both / and /boot partitions.

This will protect your SD card from any writes. Read-only, she should last for years. Be aware that all file changes will be lost at reboot. Feel free to change this setting anytime with raspi-config

sudo raspi-config
→ Performance
→ P3 Overlay File System

3) Small Partitions

First-boot auto resizing is turned off by custom cmdline.txt Small partitions are easier to handle, as in SD card cloning etc. And our kiosk wouldn’t need more data space anyway.

Should you need to more space for any reason, edit 2nd partition’s size manually and then grow your FS with resize2fs

4) Local HTML

It would’ve been for the best to load local HTML / JS app as per file:///home/pi/html/index.html and load data strictly thru APIs.

This would offer better offline handling and slightly better show up times at boot. However, I’ve opted out because of almost-zero expected downtime, non critical system and easier remote updates to the website.

Don’t forget to temporarily disable Overlay FS while uploading your HTML.

Enjoy 🥂

And don’t forget to let me know about your KioskPi projects ;-)



Creative Programmer @ ΔO []

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store