[GUIDE] Touchscreen Kiosk Dashboard on ZimaBoard 2 using Docker (X.Org + Chromium)

Hey everyone,

I wanted to share how I got a fullscreen touchscreen kiosk running on my ZimaBoard 2 connected to a 22" touch monitor via HDMI.

Since ZimaOS has no package manager (apt doesn’t exist), the trick is to do everything inside Docker containers.

My Setup

  • ZimaBoard 2 1664 (Intel N100, 16 GB RAM)

  • ZimaOS v1.5.4

  • 22" touch monitor via HDMI + USB-C→USB-A touchscreen input

The Approach

ZimaOS is Buildroot-based — no apt, no Xorg, no desktop environment. Instead, we run a Debian container with Xorg modesetting driver + Chromium in kiosk mode, with /dev/dri passed through. A separate nginx:alpine container serves the dashboard files.

Step 1 — Add your user to the docker group

Open the web terminal at http://your-zima-ip:7681 and run:

bash

sudo usermod -aG docker YOUR_USERNAME

Then reconnect SSH for the change to take effect.

Step 2 — Create the files

bash

mkdir -p /DATA/AppData/kiosk/dashboard

/DATA/AppData/kiosk/Dockerfile:

text

FROM debian:bookworm-slim
RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list
RUN apt-get update && \
apt-get install -y \
xserver-xorg-core \
xserver-xorg-input-libinput \
openbox chromium \
x11-xserver-utils xinit \
fonts-noto-core && \
apt-get install -y -t bookworm-backports libgl1-mesa-dri && \
rm -rf /var/lib/apt/lists/*
COPY xorg.conf /etc/X11/xorg.conf
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

Why backports Mesa? The Intel N100 (Alder Lake-N, PCI ID 0x46d4) is only supported from Mesa 23.0+. Debian bookworm ships 22.3. Backports provides 25.x.

/DATA/AppData/kiosk/xorg.conf:

text

**Section "Device"
Identifier "Intel"
Driver "modesetting"
Option "AccelMethod" "none"
EndSection

Section "Screen"
Identifier "Default"
Device "Intel"
EndSection

Section "InputDevice"
Identifier "Touchscreen"
Driver "libinput"
Option "Device" "/dev/input/event8"
EndSection

Section "ServerLayout"
Identifier "Default"
Screen "Default"
InputDevice "Touchscreen" "CorePointer"
EndSection**

Find your touch device event number with:

bash

cat /proc/bus/input/devices | grep -A3 -i touch

/DATA/AppData/kiosk/entrypoint.sh:

text

#!/bin/sh
Xorg :0 vt1 -s 0 -dpms -nolisten tcp &
sleep 4
export DISPLAY=:0
xset s off && xset -dpms && xset s noblank
openbox --sm-disable &
sleep 1
exec chromium \
--kiosk --no-sandbox --disable-gpu \
--disable-dev-shm-usage \
--touch-events=enabled \
--no-first-run --disable-infobars \
"${KIOSK_URL:-http://localhost:8888}"

/DATA/AppData/kiosk/nginx.conf:

text

server {
listen 8888;
root /usr/share/nginx/html;
index index.html;
}

Put your dashboard HTML/CSS/JS files into /DATA/AppData/kiosk/dashboard/.

Step 3 — Build and Run

Build (note: docker build needs --config /tmp on ZimaOS):

bash

docker --config /tmp build -t kiosk:latest /DATA/AppData/kiosk

Dashboard web server:

bash

docker --config /tmp run -d \
--name dashboard-server \
--restart unless-stopped \
--network host \
-v /DATA/AppData/kiosk/dashboard:/usr/share/nginx/html:ro \
-v /DATA/AppData/kiosk/nginx.conf:/etc/nginx/conf.d/default.conf:ro \
nginx:alpine

Kiosk display:

bash

docker --config /tmp run -d \
--name jarvis-kiosk \
--restart unless-stopped \
--privileged \
--network host \
-e KIOSK_URL=http://localhost:8888 \
-v /DATA/AppData/kiosk/xorg.conf:/etc/X11/xorg.conf:ro \
-v /DATA/AppData/kiosk/entrypoint.sh:/entrypoint.sh:ro \
-v /run/udev:/run/udev:ro \
kiosk:latest

Both containers have --restart unless-stopped so they survive reboots automatically.

Touchscreen Click Fix (important!)

Xorg’s libinput maps touch as pointer (mouse) events. Chromium then applies a tiny movement threshold for click — even a 1px finger shift prevents it from firing. If your dashboard uses onclick handlers, add this JS to replace them with pointerup + a 20px threshold:

js

(function touchFix() {
const downs = {};
document.addEventListener('pointerdown', e => {
downs[e.pointerId] = { x: e.clientX, y: e.clientY };
}, { passive: true });
function isTap(e) {
const d = downs[e.pointerId];
return !d || (Math.abs(e.clientX - d.x) < 20 && Math.abs(e.clientY - d.y) < 20);
}
function run(fn) { try { new Function(fn)(); } catch(ex) {} }
function patchButtons(root) {
root.querySelectorAll('button[onclick]').forEach(btn => {
const oc = btn.getAttribute('onclick');
btn.removeAttribute('onclick');
btn.addEventListener('pointerup', e => { e.stopPropagation(); if (isTap(e)) run(oc); });
});
}
window.addEventListener('load', () => {
document.querySelectorAll('.widget[onclick]').forEach(el => {
const oc = el.getAttribute('onclick');
el.removeAttribute('onclick');
el.addEventListener('pointerup', e => { if (isTap(e) && !e.target.closest('button')) run(oc); });
});
patchButtons(document.body);
});
new MutationObserver(muts => muts.forEach(m =>
m.addedNodes.forEach(n => { if (n.nodeType === 1) patchButtons(n); })
)).observe(document.body, { childList: true, subtree: true });
})();

Also add to your CSS to hide the mouse cursor on a touchscreen:

css

* { cursor: none !important; touch-action: manipulation; }

Troubleshooting

Problem Fix
docker build fails with unknown flag Use docker --config /tmp build …
Black screen / Basic output test failed Use Xorg modesetting, not Wayland/cage
Mesa doesn’t support N100 Install libgl1-mesa-dri from bookworm-backports
Touch input not detected Check event number with /proc/bus/input/devices, update xorg.conf
Dashboard shows ZimaOS login Port 80 is taken by ZimaOS — use a different port (8888 works)

Hope this helps someone!

2 Likes

Hi, thanks a lot for sharing this guide — it’s super helpful and very well explained. Really appreciate the effort you put into documenting everything!

Would it be okay if we repost or share your guide (with credit)? Also, do you happen to have any photos or screenshots of your setup? Would love to include them if possible.

Thanks again for contributing this! :raising_hands:

I built a power-efficient home assistant setup that’s available 24/7 without having my main rig burning electricity around the clock. The secret weapon is a Zima acting as the always-on brain, while my full PC sleeps in hibernation until it’s actually needed.

The Main PC

Here’s what’s inside the primary machine:

  • CPU: AMD Ryzen 9 9950X3D

  • Motherboard: ASUS ROG Crosshair X870E Glacial

  • RAM: Acer Predator Hermes RGB DDR5 96GB (2×48GB) 6000MHz CL28

  • Storage: Crucial T710 4TB NVMe M.2 PCIe 5.0 Gen5

  • GPU (Gaming): ASUS ROG Astral GeForce RTX 5080 16GB

  • GPU (AI): Intel Arc Pro B70 32GB (waiting on release)

  • PSU: Corsair RM1200x

  • Case: Thermaltake Tower 900

The Zima Setup (The Always-On Hub)

The Zima runs 24/7 and handles everything lightweight — home assistant duties, camera feeds, and basic AI queries using Groq free tier via the cloud.

Connected peripherals directly to the Zima:

  • Pisichen 22" Touch Screen monitor via HDMI → miniDP adapter + one USB-C cable for touch input

  • Edifier G1000 II speakers through an external USB-A to 3.5mm soundcard

  • DAMAO DGM20S microphone via USB-A

Storage:

  • Seagate IronWolf Pro 20TB — ‘‘black hole’’ NAS drive

  • Seagate IronWolf Pro 1TB — main NAS drive

Networking:

  • Zima connected to the router via Ethernet

  • Second Ethernet cable runs directly from the Zima to the PC for fast local transfers

The Smart Part: Wake-on-LAN

Here’s the magic — when someone asks a question that’s too complex for Gemini to handle well, the Zima sends a Wake-on-LAN packet to bring the main PC out of hibernation. Once it’s up, it connects to a local 70B parameter LLM running on the Intel Arc Pro B70. After the task is done, the PC goes back to sleep.

The result: a home assistant that’s always responsive, but the big rig only wakes up when there’s actual heavy lifting to do. Power bills stay sane, and I still get access to a seriously capable local model when I need it.

***Huge shoutout to Claude Code and Perplexity — honestly, without their help I would have never been able to put this whole project together on my own. They were invaluable throughout the entire process.

I hope this information helps anyone crazy enough to take on a similar project — feel free to share it wherever you like!***

1 Like