Post

Raspberry Pi Kiosk Tutorial — Boot Any Web Page Fullscreen

A no-frills guide to turning a Raspberry Pi into a fullscreen browser kiosk that loads a web page on boot. Works with any URL — local or remote.

Raspberry Pi Kiosk Tutorial — Boot Any Web Page Fullscreen

Why so many “Pi kiosk” guides go sideways

Every week someone on r/raspberry_pi asks how to make their Pi boot straight into a fullscreen web page on an HDMI display. The usual answers either assume you have a desktop environment (so the cursor sits there forever, the screen blanks, Chromium nags about restoring sessions, the keyboard never appears on touchscreens) or they wire up Wayland/Cage in ways that fight the GPU on the Pi 5.

This post is the short, opinionated version that actually works on Raspberry Pi OS Lite with no desktop environment installed. It loads any URL you give it. If you later want to use the Pi to display IoT dashboards or workflow visualizations, see the companion post that uses this same technique to display Krill Server SVG dashboards.

The hard rules (do not skip these)

  1. Use Raspberry Pi OS Lite — no desktop. A desktop environment brings a cursor, screen blanking, notifications, and a session manager that all fight you. Flash Lite from the Pi Imager.
  2. Use X11, not Wayland. Xorg’s -nocursor flag completely disables the mouse pointer at the server level. Every “hide the cursor” hack on Wayland (unclutter, transparent themes, etc.) is unreliable.
  3. Run as a dedicated unprivileged user, not pi. This keeps the kiosk session isolated and lets you SSH in as your normal user without disturbing the display.
  4. Disable screen blanking and DPMS at the X server level every boot — xset s off, xset s noblank, xset -dpms. The “raspi-config” toggle is not enough.
  5. For touchscreens, install onboard and pre-seed it with dconf so it auto-shows on text input without a desktop session manager.

What you need

  • Raspberry Pi 4 or 5 (Pi 5 strongly recommended for Chromium)
  • HDMI display (touchscreen optional)
  • A fresh install of Raspberry Pi OS Lite (64-bit)
  • A URL you want to display — anything reachable from the Pi

Step 1 — Install the bare minimum

After flashing Lite and SSHing in:

1
2
3
4
sudo apt update
sudo apt install -y \
    xserver-xorg xserver-xorg-input-libinput xinit x11-xserver-utils \
    chromium fonts-liberation ca-certificates

For touchscreens, also install the on-screen keyboard:

1
sudo apt install -y onboard dconf-cli at-spi2-core

Step 2 — Create a kiosk user

1
sudo useradd -m -s /bin/bash -G video,audio,input,render,tty kiosk

Step 3 — Allow non-root X

1
2
3
4
sudo tee /etc/X11/Xwrapper.config >/dev/null <<'EOF'
allowed_users=anybody
needs_root_rights=yes
EOF

Step 4 — Tell Xorg which GPU card to use (Pi 4 and Pi 5)

Under vc4-kms-v3d the Pi exposes two DRM cards: one for the vc4 display controller and one for the v3d 3D renderer — typically card0 and card1, but the ordering is not guaranteed on either model. Point Xorg at the display card explicitly:

Do this on the Pi 4 too. Earlier versions of this post marked this step “Pi 5 only” because that’s where it fails loudly — Xorg refuses to start with “Cannot run in framebuffer mode.” On the Pi 4 the same ambiguity tends to fail quietly instead: Xorg comes up on a default screen size rather than the panel’s, and your page ends up rendered into a corner of the display.

1
2
3
4
5
6
7
8
sudo mkdir -p /etc/X11/xorg.conf.d
sudo tee /etc/X11/xorg.conf.d/99-kiosk.conf >/dev/null <<'EOF'
Section "Device"
    Identifier "Kiosk Display"
    Driver     "modesetting"
    Option     "kmsdev" "/dev/dri/card0"
EndSection
EOF

If card0 isn’t the vc4 card on your unit, find the right one with:

1
2
3
for c in /sys/class/drm/card*; do
    grep -l "DRIVER=vc4-drm" "$c/device/uevent" 2>/dev/null
done

Step 5 — The kiosk launch script

Drop this at /home/kiosk/kiosk.sh (replace KIOSK_URL with your page):

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
#!/bin/bash
KIOSK_URL="https://example.com"

# Wait until the URL responds (handy if it's a service on the same Pi)
for i in {1..30}; do
    curl -sk "$KIOSK_URL" >/dev/null 2>&1 && break
    sleep 1
done

# Kill screen blanking and power management
xset s off
xset s noblank
xset -dpms

# Pin the display geometry before Chromium starts.
# There is no window manager here, so --kiosk / --start-fullscreen cannot go
# fullscreen via EWMH — Chromium sizes its own window from whatever RandR
# reports at startup, and nothing corrects it afterward. On a Pi 4 that guess is
# often wrong: two HDMI connectors make Xorg build a double-width screen, and a
# slow EDID read can leave a stale default size. Either way the page renders
# into a fraction of the panel. Assert the geometry instead of trusting it.
KIOSK_W=""
KIOSK_H=""
if command -v xrandr >/dev/null 2>&1; then
    PRIMARY=$(xrandr -q | awk '/ connected/ { print $1; exit }')
    if [ -n "$PRIMARY" ]; then
        # Disable every other output so the X screen is the panel itself, not a
        # bounding box spanning both HDMI ports.
        xrandr -q | awk '/ connected/ { print $1 }' | grep -vx "$PRIMARY" | while read -r output; do
            xrandr --output "$output" --off 2>/dev/null || true
        done
        xrandr --output "$PRIMARY" --primary --auto --pos 0x0 2>/dev/null || true
        KIOSK_GEOM=$(xrandr -q | awk -v o="$PRIMARY" '$1 == o {
            for (i = 1; i <= NF; i++)
                if ($i ~ /^[0-9]+x[0-9]+\+[0-9]+\+[0-9]+$/) { split($i, a, "+"); print a[1]; exit }
        }')
        if [ -n "$KIOSK_GEOM" ]; then
            KIOSK_W=${KIOSK_GEOM%x*}
            KIOSK_H=${KIOSK_GEOM#*x}
            xrandr --fb "${KIOSK_W}x${KIOSK_H}" 2>/dev/null || true
            echo "Kiosk display: $PRIMARY ${KIOSK_W}x${KIOSK_H}"
        fi
    fi
fi

# Only pass explicit bounds when we actually resolved them; otherwise fall back
# to Chromium's own sizing.
CHROME_GEOMETRY=()
if [ -n "$KIOSK_W" ] && [ -n "$KIOSK_H" ]; then
    CHROME_GEOMETRY=(--window-position=0,0 --window-size="$KIOSK_W,$KIOSK_H")
fi

# (touchscreen only) start the on-screen keyboard
if command -v onboard >/dev/null; then
    dconf write /org/gnome/desktop/a11y/applications/screen-keyboard-enabled true 2>/dev/null || true
    onboard &
fi

exec chromium \
    --kiosk \
    --noerrdialogs \
    --disable-infobars \
    --disable-session-crashed-bubble \
    --disable-translate \
    --no-first-run \
    --disable-pinch \
    --overscroll-history-navigation=0 \
    --autoplay-policy=no-user-gesture-required \
    --start-fullscreen \
    --password-store=basic \
    --disable-component-update \
    --check-for-update-interval=31536000 \
    "${CHROME_GEOMETRY[@]}" \
    "$KIOSK_URL"
1
2
sudo chown kiosk:kiosk /home/kiosk/kiosk.sh
sudo chmod +x /home/kiosk/kiosk.sh

Why the xrandr block is there

There is no window manager in this setup — xinit launches Chromium and nothing else. That means --kiosk and --start-fullscreen cannot go fullscreen the normal way (via the EWMH _NET_WM_STATE_FULLSCREEN hint, which needs a window manager to honor it). Chromium has to size its own window from whatever RandR reports when it starts, and nothing ever corrects it afterward.

That is fine when RandR reports the panel. It frequently doesn’t on a Pi 4:

  • Two HDMI connectors. Xorg’s modesetting driver auto-enables every connected output side by side, so the X screen becomes e.g. 3840x1080. Chromium lays the page out at 3840 wide and the panel shows the left half of it. You don’t need a second monitor for this — phantom hotplug on the unused port (HDMI-to-VGA adapters, splitters, some AV receivers) is enough.
  • A lost mode-negotiation race. If EDID reading finishes after X starts, RandR resizes the screen and — with no window manager — Chromium’s window stays at the old size. The Pi 4 loses this race far more often than a Pi 5.

The block turns off every output except the first connected one, pins the framebuffer to that output’s mode, and hands Chromium explicit --window-position / --window-size bounds. If xrandr isn’t available or reports nothing usable, the flags are omitted and Chromium sizes itself exactly as before.

For a stubborn display you can also force the mode before X ever starts, by appending this to the single line in /boot/firmware/cmdline.txt (substitute your connector and mode):

1
video=HDMI-A-1:1920x1080@60

That settles KMS at boot, which removes the race entirely.

Step 6 — The launcher (starts X with no cursor)

This is the magic line. -nocursor tells the X server to never render a pointer at all — the only reliable way to hide it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sudo tee /usr/local/bin/kiosk-launcher >/dev/null <<'EOF'
#!/bin/bash
KIOSK_USER="kiosk"
KIOSK_UID=$(id -u "$KIOSK_USER")

mkdir -p "/run/user/$KIOSK_UID"
chown "$KIOSK_USER:$KIOSK_USER" "/run/user/$KIOSK_UID"
chmod 700 "/run/user/$KIOSK_UID"

# Transfer tty7 ownership so Xorg can open it O_RDWR.
# On Pi OS Lite, /dev/tty7 is root:tty crw--w---- — the tty group
# has write-only access, which fails Xorg's open(O_RDWR).
# systemd's TTYReset=yes restores ownership on exit.
chown "$KIOSK_USER" /dev/tty7

chvt 7

exec su - "$KIOSK_USER" -c "
    export XDG_RUNTIME_DIR=/run/user/$KIOSK_UID
    exec xinit /home/$KIOSK_USER/kiosk.sh -- :0 vt7 -nocursor -nolisten tcp
"
EOF
sudo chmod +x /usr/local/bin/kiosk-launcher

Step 7 — Run it on boot with systemd

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
sudo tee /etc/systemd/system/kiosk.service >/dev/null <<'EOF'
[Unit]
Description=Raspberry Pi Kiosk (X11)
After=network-online.target
Wants=network-online.target
ConditionPathExists=/dev/dri/card0

[Service]
Type=simple
ExecStartPre=/bin/sleep 3
ExecStart=/usr/local/bin/kiosk-launcher
TTYPath=/dev/tty7
TTYReset=yes
TTYVHangup=yes
TTYVTDisallocate=yes
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable kiosk.service
sudo systemctl start kiosk.service

Reboot. The Pi should boot straight into your URL, no cursor, no UI chrome, no desktop.

“But what page should I show?”

If you don’t already have something to display, here are dirt-simple ways to host a page on the Pi itself so the kiosk just loads http://localhost:

Option A — A static HTML file with Python

1
2
3
mkdir -p ~/site && cd ~/site
echo '<h1 style="font:10vw sans-serif">Hello kiosk</h1>' > index.html
python3 -m http.server 80

Set KIOSK_URL="http://localhost" in kiosk.sh. Wrap the python3 line in its own systemd service if you want it to survive reboot.

Option B — Nginx serving a folder

1
2
3
sudo apt install -y nginx
sudo rm /var/www/html/index.nginx-debian.html
echo '<h1>It works</h1>' | sudo tee /var/www/html/index.html

Option C — A Grafana / Home Assistant / Node-RED / etc. dashboard

Point KIOSK_URL at whatever service you already run (http://localhost:3000, http://homeassistant.local:8123/lovelace/0?kiosk, etc.). Most dashboards have a ?kiosk URL parameter that strips their own chrome — combine that with this guide and you get a true fullscreen display.

Going further — IoT and workflow dashboards

If you want to display real-time sensor data, GPIO state, automations, or custom SVG diagrams that update live (think factory-floor mimic boards, water systems, lab experiments), the same kiosk technique above is exactly what powers the Krill Server kiosk mode. The Krill installer bundles all of the steps in this post into a single krill_kiosk_install.sh script, plus certificate trust, an authenticated WebAssembly client, and a binding system that lets you wire SVG shapes to live data with a tag like id="k_pin22".

If that sounds useful, read the companion post: Turn a Raspberry Pi Into a Kiosk With Custom SVG Dashboards.

Troubleshooting

SymptomFix
Cursor still visibleYou forgot -nocursor on the xinit line, or you’re on Wayland.
Screen blanks after 10 minxset calls didn’t run — make sure they’re in the script that xinit launches, not in .bashrc.
xf86OpenConsole: Cannot open virtual console 7 (Permission denied)chown kiosk /dev/tty7 must run before chvt 7 in the launcher (as root). On Pi OS Lite, /dev/tty7 is root:tty crw--w---- — the tty group has write-only access, which fails Xorg’s open(O_RDWR). The Step 6 heredoc already includes this; if you’re using an older launcher, add it manually.
Xorg fails on Pi 5Add the 99-kiosk.conf from Step 4 pointing at the vc4 card.
Page fills only part of the screen (typically the left half or a corner)RandR isn’t reporting the panel. Check with DISPLAY=:0 xrandr --listmonitors — a width twice the panel’s means both HDMI outputs are enabled; a small size like 1024x768 means Xorg never picked up the real mode. The xrandr block in Step 5 fixes both; also apply Step 4 on the Pi 4, and see the cmdline.txt note under Step 5.
Chromium “restore session” bubbleThe --disable-session-crashed-bubble and --no-first-run flags handle it; if it persists, delete ~/.config/chromium.
On-screen keyboard never appearsMake sure at-spi2-core is installed and the dconf write line ran. The keyboard only auto-shows when the page focuses a real <input>.
Page loads before service is readyThe for i in {1..30} curl loop in kiosk.sh waits for it. Tune the loop count if your service is slow to start.

Uninstall

1
2
3
4
5
sudo systemctl disable --now kiosk.service
sudo rm /etc/systemd/system/kiosk.service /usr/local/bin/kiosk-launcher
sudo rm /etc/X11/Xwrapper.config /etc/X11/xorg.conf.d/99-kiosk.conf
sudo userdel -r kiosk
sudo apt remove --purge -y chromium xserver-xorg xinit onboard

Last verified: 2026-04-14 on a Raspberry Pi 5. Updated 2026-09-24: Step 4 now applies to the Pi 4 as well, and Step 5 pins the display geometry explicitly — see krill#1104.

This post is licensed under CC BY 4.0 by Sautner Studio, LLC.