Here’s how to turn a Raspberry Pi into a touchscreen kiosk running Ubuntu and Home Assistant.
This guide uses modern Wayland (Sway + Chromium). Alternatives like X11-based FullPageOS exist, but Wayland is more future-proof.
The end result is a device that:
- Boots straight into fullscreen Home Assistant
- Auto-starts on power-up, no login required
- Turns the display off after idle, wakes instantly on touch
- Supports a handy three-finger tap to reload when Wi-Fi or HA hangs
Hardware used:
Flash & Prepare Ubuntu
- Flash Ubuntu Server for Raspberry Pi (24.04.3 LTS or newer) using Raspberry Pi Images.
- Configure your
user-data
and network-config
before first boot (see Configure Cloud Init). - Boot the Pi and let
cloud-init
configure everything automatically. - Set locale and timezone for correct time and language:
1
2
| sudo timedatectl set-timezone Europe/Berlin
sudo update-locale LANG=en_US.UTF-8
|
user-data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| #cloud-config
# Hostname
hostname: ubuntu
manage_etc_hosts: true
# Default user with sudo rights
users:
- name: ubuntu
groups: sudo,adm
shell: /bin/bash
sudo: ["ALL=(ALL) ALL"]
ssh-authorized-keys:
- ssh-ed25519 AAAA...your-ssh-key
# Lock out password-based SSH login for security
ssh_pwauth: false
# Expire default password (forces password change if ever used)
chpasswd:
expire: true
users:
- name: ubuntu
password: ubuntu
type: text
# Fix Wi-Fi Stability (Broadcom driver)
write_files:
- path: /etc/modprobe.d/brcmfmac.conf
content: |
options cfg80211 ieee80211_regdom=DE
options brcmfmac power_save=0
options brcmfmac feature_disable=0x82000
|
network-config
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| network:
version: 2
ethernets:
eth0:
optional: true
dhcp4: true
dhcp6: false
wifis:
wlan0:
optional: false
dhcp4: true
dhcp6: false
access-points:
"your-ssid":
password: "your-wifi-password" # pragma: allowlist secret
regulatory-domain: DE
|
If cloud-init doesn’t pick up changes, reapply configs manually:
1
2
| sudo rm /var/lib/cloud/data/instance-id
sudo cloud-init init --local
|
Secure Ubuntu (Optional)
To harden your Pi, you can run my Ansible security playbooks. This step is optional but recommended.
1
2
3
4
5
6
| git clone --recursive https://github.com/MVladislav/ansible-env-setup.git
cd ansible-env-setup
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install -r requirements.txt
ansible-galaxy collection install --upgrade -r requirements.yml
|
Create Inventory
Replace:
<SSH_PRIV_KEY_FILENAME>
: Path to your private ssh key e.g. ~/.ssh/example
<SSH_PUB_KEY_STRING>
: Add the public key content e.g. cat ~/.ssh/example.pub
<PI_IP_ADDRESS>
: IP address from your Pi<PI_USER_PASSWORD>
: Can be created with sudo apt install whois && mkpasswd --method=sha-512
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
| tee ./inventory/inventory-pi.yml <<'EOF'
all:
hosts:
server:
cis_ubuntu2404_rule_1_1_1_9: false
cis_ubuntu2404_rule_1_3_1_2: false # no grub available
cis_ubuntu2404_rule_3_1_1: false # no grub available
cis_ubuntu2404_rule_6_2_1_3: false # no grub available
cis_ubuntu2404_rule_6_2_1_4: false # no grub available
## ANSIBLE DEFAULTS
## _______________________
ansible_ssh_private_key_file: ~/.ssh/<SSH_PRIV_KEY_FILENAME>
pl_a_cis_setup_ssh_pub_key: "<SSH_PUB_KEY_STRING>"
ansible_user: ubuntu
ansible_host: <PI_IP_ADDRESS>
ansible_port: 22
ansible_host_hostname: "kiosk"
pl_a_service_name: "server_"
# NOTE: change to your required dns and ntp server, or change in playbooks
pl_a_host_default_ntp: time.cloudflare.com
pl_a_host_fallback_ntp: ntp.ubuntu.com
## ANSIBLE CLIENT ACCESS SETUP
## _______________________
pl_a_user_setup: true
pl_a_user_config:
is_update: true
password: "<PI_USER_PASSWORD>"
group: sudo,adm,dip,lxd,plugdev
system: false
append: true
add_ssh_key: true
## UPDATER
## _______________________
updater_time_timezone: Europe/Berlin
updater_ntp_server: ""
updater_ntp_fallback_server: ""
## NETPLAN
## _______________________
pl_a_netplan_setup: false
netplan_remove_existing: false
## SSH
## NOTE: ssh not needed, is general setup by CIS, here only default conf to setup per user defaults
## _______________________
pl_a_ssh_setup: true
security_ssh_only_client_setup: true
## UFW
## _______________________
## NOTE: ufw is general setup by CIS, use here to set logging level and ssh if not setup by cis
pl_a_ufw_setup_enable: true
pl_a_ufw_logging_level: "off" # off | low | medium
## SERVICES (disable to not setup)
## _______________________
pl_a_snmp_setup: false
pl_a_nullmailer_setup: false
pl_a_postfix_setup: false
pl_a_hardening_scan_setup: false
## CIS
## _______________________
pl_a_cis_setup: true
pl_a_cis_setup_aide: false
pl_a_cis_ipv6_required: true
## HARDENING SECURITY
## _______________________
pl_a_auditd_setup: true
pl_a_fail2ban_setup: true
## ---------------------------------------------------------------------------
vars:
ansible_python_interpreter: /usr/bin/python3
children:
servers:
hosts:
server:
EOF
|
Create PlayBook SSH-Default
Replace:
<SSH_PUB_KEY_STRING>
: Add the public key content e.g. cat ~/.ssh/example.pub
<PI_IP_ADDRESS>
: IP address from your Pi
1
2
3
4
5
6
7
| tee ./playbooks/vars/default.yml <<'EOF'
keystodeploy:
# id_ed25519
"ubuntu-<PI_IP_ADDRESS>":
- sshkey: <SSH_PUB_KEY_STRING>
EOF
|
Run PlayBook
1
| ansible-playbook ./playbooks/playbook-sec-short.yml --ask-become-pass -k -i ./inventory/inventory-pi.yml
|
We’ll use Chromium in kiosk mode (Wayland + Sway). Firefox can also be used, but Chromium tends to run smoother on the Pi.
1
2
| sudo apt update
sudo apt install -y sway swayidle wayland-utils
|
Install Chromium
1
| sudo snap install chromium
|
Create a Kiosk User
1
2
3
| sudo adduser kiosk
sudo usermod -aG video,input,render,audio kiosk
sudo loginctl enable-linger kiosk
|
Auto-Login & Auto-Start Kiosk
Enable autologin for the kiosk
user:
1
2
3
4
5
6
7
| sudo mkdir -p /etc/systemd/system/getty@tty1.service.d/
sudo tee /etc/systemd/system/getty@tty1.service.d/override.conf <<'EOF'
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin kiosk --noclear %I $TERM
EOF
|
Systemd User Service for Sway
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| sudo -u kiosk mkdir -p /home/kiosk/.config/systemd/user
sudo -u kiosk tee /home/kiosk/.config/systemd/user/sway.service <<'EOF'
[Unit]
Description=Sway - Wayland compositor
Documentation=man:sway(5) man:sway(1)
PartOf=graphical.target
After=graphical.target
[Service]
Environment=XDG_RUNTIME_DIR=%t
Restart=always
RestartSec=2
ExecStart=/usr/bin/sway --unsupported-gpu
[Install]
WantedBy=default.target
EOF
# Enable + start service
sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) systemctl --user daemon-reload
sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) systemctl --user --now enable sway.service
|
Create Sway config:
Replace placeholders:
<DISPLAY-OUTPUT>
: your HDMI output - check possible option with:1
2
3
4
| sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) \
SWAYSOCK=$(sudo -u kiosk find /run/user/$(id -u kiosk) \
-type s -name 'sway-ipc.*.sock' 2>/dev/null | head -n1) \
swaymsg -t get_outputs | jq '.[] | .name'
|
input "8746:1:ILITEK_ILITEK-TP"
: your touch input - check possible option with:1
2
3
4
| sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) \
SWAYSOCK=$(sudo -u kiosk find /run/user/$(id -u kiosk) \
-type s -name 'sway-ipc.*.sock' 2>/dev/null | head -n1) \
swaymsg -t get_inputs | jq '.[] | select(.type == "touch") | .identifier'
|
1024x600
: your screen resolution - check possible option with:1
2
3
4
| sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) \
SWAYSOCK=$(sudo -u kiosk find /run/user/$(id -u kiosk) \
-type s -name 'sway-ipc.*.sock' 2>/dev/null | head -n1) \
swaymsg -t get_outputs
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| sudo -u kiosk mkdir -p /home/kiosk/.config/sway
sudo -u kiosk tee /home/kiosk/.config/sway/config <<'EOF'
xwayland disable
set $mod Mod4
set $term foot
set $menu dmenu_path | wmenu | xargs swaymsg exec --
output * bg /usr/share/backgrounds/sway/Sway_Wallpaper_Blue_1920x1080.png fill
### Idle handling with dpms + touch fix ###
exec swayidle -w \
timeout 60 'swaymsg "output * dpms off"' \
resume 'swaymsg "output * dpms on"'
### Screen & input mapping ###
output <DISPLAY-OUTPUT> {
resolution 1280x800
transform 90
}
input "8746:1:ILITEK_ILITEK-TP" {
map_to_output <DISPLAY-OUTPUT>
tap enabled
natural_scroll enabled
middle_emulation enabled
}
### Cursor hiding ###
seat * hide_cursor 1000
include /etc/sway/config.d/*
EOF
sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) systemctl --user restart sway.service
|
Systemd User Service for Browser
Replace placeholders:
<URL-TO-DASHBOARD>
: e.g. https://home.local:8123/lovelace/kiosk
wayland-1
: find correct DISPLAY index - check possible option with:1
| sudo -u kiosk ls "/run/user/$(sudo -u kiosk id -u)" | grep wayland
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
| sudo -u kiosk mkdir -p /home/kiosk/.config/systemd/user
sudo -u kiosk tee /home/kiosk/.config/systemd/user/browser-kiosk.service <<'EOF'
[Unit]
Description=Browser in kiosk mode
After=graphical.target sway.service
PartOf=sway.service
[Service]
Environment=WAYLAND_DISPLAY=wayland-1
Environment=XDG_RUNTIME_DIR=%t
Restart=always
RestartSec=1
StartLimitIntervalSec=60
StartLimitBurst=5
ExecStart=/snap/bin/chromium --kiosk \
--app=<URL-TO-DASHBOARD> \
--enable-features=VaapiVideoDecoder,CanvasOopRasterization,UseOzonePlatform,WaylandWindowDecorations \
--ozone-platform=wayland \
--use-gl=egl \
--enable-gpu-rasterization \
--ignore-gpu-blocklist \
--overscroll-history-navigation=0 \
--disable-translate \
--disable-session-crashed-bubble \
--noerrdialogs --no-first-run --disable-infobars \
--load-extension=/home/kiosk/reload-on-touch
[Install]
WantedBy=default.target
EOF
sudo -u kiosk tee /home/kiosk/.config/systemd/user/browser-kiosk-restart.service <<'EOF'
[Unit]
Description=Restart the browser kiosk service
[Service]
Type=oneshot
ExecStart=/usr/bin/systemctl --user restart browser-kiosk.service
EOF
sudo -u kiosk tee /home/kiosk/.config/systemd/user/browser-kiosk-restart.timer <<'EOF'
[Unit]
Description=Restart browser kiosk daily at 03:00
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
EOF
|
Add Reload-on-Touch Extension
Chromium extension for three-finger reload:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| sudo -u kiosk mkdir -p /home/kiosk/reload-on-touch
sudo -u kiosk tee /home/kiosk/reload-on-touch/manifest.json <<'EOF'
{
"manifest_version": 3,
"name": "Reload on Touch",
"version": "1.0",
"permissions": ["activeTab"],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["touch.js"]
}
]
}
EOF
sudo -u kiosk tee /home/kiosk/reload-on-touch/touch.js <<'EOF'
// Reload page on three-finger tap
document.addEventListener("touchstart", (e) => {
if (e.touches.length === 3) {
location.reload();
}
});
EOF
|
Enable and start
1
2
3
| sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) systemctl --user daemon-reload
sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) systemctl --user --now enable browser-kiosk.service
sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) systemctl --user --now enable browser-kiosk-restart.timer
|
Disable Cloud-init (Optional)
You can also just leave it enabled; after first boot it won’t change much unless re-provisioned.
1
| sudo touch /etc/cloud/cloud-init.disabled
|
🔧 Troubleshooting & Tips
- Wi-Fi drops: verify
dmesg | grep brcmfmac
for errors, adjust options in /etc/modprobe.d/brcmfmac.conf
. - Slow Snap apps: prefer apt-installed Chromium/Firefox when possible.
- Check service logs on problems:
sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) journalctl --user -u sway.service -f
sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) journalctl --user -u browser-kiosk.service -f
- Kiosk stuck or blank?
- Switch to another TTY with
Ctrl+Alt+F2
to get a shell for troubleshooting. - You can disable kiosk mode by editing/removing the systemd user services for debugging.