Case study — Systems & self-hosting
Building a fully offline, self-hosted smart-home assistant
A private, cloud-free voice assistant and smart-home hub running on a single Linux laptop.
Role — Architecture, integration & debugging
Stack — Linux Mint · Vosk · Ollama (gemma3:4b) · Piper · MagicMirror² · Jellyfin · Tailscale
Status — Running 24/7, used daily by the household
Add hero image or video — the MagicMirror display in the kitchen
Overview
I built a self-hosted smart-home hub around a kitchen-mounted MagicMirror² display, including a fully offline voice assistant (“Dwight”) that listens, understands and speaks without any internet connection or third-party account. It runs 24/7 on one Dell Inspiron laptop and is used daily by the household. Everything — speech recognition, the AI brain, and the voice — runs locally on the machine.
This was a systems project: choosing an architecture, integrating a stack of independent services, and — the part that actually mattered — diagnosing and fixing things when they broke.
Add photo — the assistant’s animated face on the display
The brief I set myself
Before building anything, I set constraints:
Fully local. No cloud accounts, no credit cards on file, no voice data leaving the house. Privacy was the point, not an afterthought.
Always-on and unattended. A kitchen display the household actually relies on, which survives reboots and power cuts without me babysitting it.
Runs on hardware I already owned. One Dell laptop, CPU only — no GPU, no Raspberry Pi cluster.
Extensible. New capabilities (a new command, a new device) should slot in without re-architecting the whole thing.
Defining these up front is what made the technical decisions easy later — every choice got measured against them.
Architecture
A stack of independent, self-hosted services, tied together on Linux Mint and kept alive by a process manager:
MagicMirror² — the always-on kitchen display, running under pm2 so it auto-restarts and persists across reboots.
Vosk — offline wake-word detection and speech-to-text. No API key, no account.
Ollama (gemma3:4b) — the local LLM that interprets what was said and decides what to do.
Piper — fast, natural offline text-to-speech for spoken replies.
Jellyfin — self-hosted media server.
node-sonos-http-api — controls the household Sonos speakers over the local network.
Tailscale — private networking for secure remote access to the stack (including a self-hosted music setup that streams to the car via CarPlay).
On top of this I built several custom MagicMirror modules — an animated face for the assistant, a morning AI briefing, a daily word-of-the-day, and a listening indicator — so the display reflects what Dwight is doing in real time.
Add architecture diagram — Vosk → Ollama → Piper pipeline
Key decisions
Offline pipeline over off-the-shelf. The standard voice modules were a dead end: the classic option (Snowboy) was discontinued, and the “easy” ones depend on Google Assistant or Alexa — cloud accounts and your voice shipped to a third party, the opposite of the brief. So I built the pipeline myself: Vosk → Ollama → Piper. Every stage runs locally, costs nothing to run, and depends on no outside service.
gemma3:4b as the model. Small enough to respond quickly on CPU-only laptop hardware, while still handling intent parsing reliably. A bigger model would have been more capable but too slow for a device that has to feel instant in a kitchen.
One reusable integration pattern. Voice commands map to actions through a single fetch/POST pattern against local REST APIs. Sonos control was the first implementation; the handler was deliberately built so new devices (e.g. smart plugs via Home Assistant) drop into the same structure instead of each needing bespoke code.
Problems I solved
The build was the easy half. Here’s the debugging that taught me the most.
A fan that wouldn’t calm down. The laptop’s fan kept slamming on loudly and cycling. My first assumption was CPU load — MagicMirror (Electron), Ollama and the voice stack all running at once. top did show Electron at 40%+, so it looked plausible. But disabling modules didn’t fix it. Running sensors told the real story: the fan was spinning at 8,733 RPM against a 4,900 RPM rated maximum, while core temperatures sat at a completely normal 38–46°C. So it wasn’t heat at all — the fan controller was misbehaving. The root cause was in the BIOS: Intel SpeedStep had been disabled, so the CPU never clocked down at idle. Re-enabling it dropped the fan to 4,198 RPM. The lesson: measure before you assume — the obvious hypothesis was wrong, and the numbers pointed somewhere else entirely.
A wake word with a hair trigger. After adding alternate spellings so Vosk would reliably catch the wake word, the assistant started waking up on half the kitchen’s conversations. The cause was that the alternates I’d added were common English sounds, so ordinary talk kept triggering it. The fix was to understand why it was over-triggering rather than just tweak thresholds: I moved to a two-token wake phrase (“hello Dwight”), removed the noisy homophones, and added explicit stop and timeout commands (“stop”, “enough”, “Dwight stop for an hour”) so it could be silenced on demand.
Surviving reboots cleanly. After a BIOS change and reboot, pm2 came back with an empty process list and the mirror didn’t start. Getting it reliably persistent meant launching MagicMirror with the correct DISPLAY=:0 environment and running pm2 save so the process list is restored automatically on every boot — the difference between a demo and something the household can actually depend on.
Add screenshots — sensors output and fan RPM before/after
What it does now
Running unattended, Dwight can set kitchen timers, add items to a shopping list and to Todoist, control Sonos playback by voice, deliver a spoken morning briefing, and sleep/wake the display — all offline, with a custom animated face reacting on the mirror as it listens and replies.
What this demonstrates
Linux administration and self-hosting a multi-service stack
Deploying and running a local LLM on constrained hardware
Systematic debugging — forming a hypothesis, measuring, ruling it out, finding the real root cause
Defining requirements up front and designing to them
Integration and API work with a reusable pattern for extensibility
Process management and building for unattended, always-on reliability
Networking (Tailscale) for secure remote access
A note on how it was built
I built this using an AI coding tool (Claude Code) to write and edit the code on the machine. My work was the architecture, the integration decisions, and — above all — the debugging: understanding why each part behaves as it does and fixing it when it doesn’t. The SpeedStep diagnosis, the wake-word cause, the model choice and the persistence setup were mine to reason through; the AI couldn’t do that part for me. Working effectively with these tools to ship real, working software is itself the point.