<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Natsuki</title>
    <description>The latest articles on DEV Community by Natsuki (@tsukiyo).</description>
    <link>https://dev.to/tsukiyo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2200581%2Fe6a6415a-99cd-42b8-a9f0-3e77ca8f2848.jpeg</url>
      <title>DEV Community: Natsuki</title>
      <link>https://dev.to/tsukiyo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tsukiyo"/>
    <language>en</language>
    <item>
      <title>Why I Run Nginx on My OpenWrt Router (And How You Can Too)</title>
      <dc:creator>Natsuki</dc:creator>
      <pubDate>Fri, 16 Jan 2026 07:21:41 +0000</pubDate>
      <link>https://dev.to/tsukiyo/why-i-run-nginx-on-my-openwrt-router-and-how-you-can-too-4h5i</link>
      <guid>https://dev.to/tsukiyo/why-i-run-nginx-on-my-openwrt-router-and-how-you-can-too-4h5i</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Your router already handles all network traffic. Why not let it handle reverse proxying too? Running nginx on OpenWrt eliminates an extra hop, simplifies your network architecture, and gives you a single point of SSL termination. This post covers why OpenWrt is the right choice, what nginx actually does, and how to configure it using OpenWrt’s UCI system to proxy 25+ homelab services with clean subdomain URLs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📦 This nginx setup runs on my DeskPi 12U homelab in a 10sqm Tokyo apartment. See the full setup: &lt;a href="https://dev.to/tsukiyo/small-but-mighty-homelab-deskpi-12u-running-20-services-4l7f"&gt;Small But Mighty Homelab: DeskPi 12U Running 20+ Services&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Contents:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Why OpenWrt?&lt;/li&gt;
&lt;li&gt;What is Nginx and Why Do You Need It?&lt;/li&gt;
&lt;li&gt;Why Run Nginx on Your Router?&lt;/li&gt;
&lt;li&gt;Installing Nginx on OpenWrt&lt;/li&gt;
&lt;li&gt;Understanding OpenWrt’s UCI Configuration&lt;/li&gt;
&lt;li&gt;Setting Up Wildcard SSL Certificates&lt;/li&gt;
&lt;li&gt;Adding Your First Service&lt;/li&gt;
&lt;li&gt;DNS Configuration with dnsmasq&lt;/li&gt;
&lt;li&gt;Real Example: 25 Services Proxied Through One Router&lt;/li&gt;
&lt;li&gt;Tips and Considerations&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1. Why OpenWrt?
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Before diving into nginx, let’s talk about why OpenWrt is the foundation that makes this setup possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenWrt&lt;/strong&gt; is a Linux distribution designed for embedded devices, primarily routers. Unlike the locked-down firmware that comes with consumer routers, OpenWrt gives you a full Linux system with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Package management&lt;/strong&gt; : Install what you need, remove what you don’t&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSH access&lt;/strong&gt; : Full command-line control over your network&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customizable firewall&lt;/strong&gt; : iptables/nftables with granular control&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real services&lt;/strong&gt; : Run nginx, wireguard (for remote access to your homelab), adblock, and more natively&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most consumer router firmware is a black box. You get a web UI with limited options and no way to extend functionality. OpenWrt turns your router into a proper Linux server that happens to also route packets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hardware Matters
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Not all routers can run OpenWrt well. You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sufficient RAM&lt;/strong&gt; : 256MB minimum, 512MB+ recommended for nginx&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt; : Internal flash or USB storage for configs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU&lt;/strong&gt; : Modern ARM or MIPS processors handle nginx easily&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check the &lt;a href="https://openwrt.org/toh/start" rel="noopener noreferrer"&gt;OpenWrt Table of Hardware&lt;/a&gt; to see if your router is supported.&lt;/p&gt;

&lt;p&gt;I’m running the &lt;strong&gt;OpenWrt One&lt;/strong&gt; - a router specifically designed for OpenWrt with 1GB RAM and 256MB NAND storage. It runs nginx with 25+ reverse proxy configurations without breaking a sweat.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. What is Nginx and Why Do You Need It?
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nginx&lt;/strong&gt; (pronounced “engine-x”) is a high-performance web server and reverse proxy. In a homelab context, you’ll primarily use it as a &lt;strong&gt;reverse proxy&lt;/strong&gt; - a server that sits between your clients and your backend services.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem Nginx Solves
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Without a reverse proxy, accessing your homelab services looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Grafana: &lt;code&gt;http://192.168.1.201:3000&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Home Assistant: &lt;code&gt;http://192.168.1.213:8123&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Jellyfin: &lt;code&gt;http://192.168.1.201:8096&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach has several issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Memorizing IPs and ports&lt;/strong&gt; is tedious&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No HTTPS&lt;/strong&gt; means credentials sent in plaintext&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port conflicts&lt;/strong&gt; when services want the same port&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No centralized access control&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With nginx as a reverse proxy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Grafana: &lt;code&gt;https://grafana.raspberrypi.home&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Home Assistant: &lt;code&gt;https://homeassistant.raspberrypi.home&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Jellyfin: &lt;code&gt;https://jellyfin.raspberrypi.home&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How Reverse Proxying Works
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgwany0ou6nqgj9npszjm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgwany0ou6nqgj9npszjm.png" alt="Reverse_Proxy_Diagram" width="800" height="87"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client requests &lt;code&gt;https://grafana.raspberrypi.home&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;DNS resolves to your router’s IP (192.168.1.1)&lt;/li&gt;
&lt;li&gt;Nginx receives the request, terminates SSL&lt;/li&gt;
&lt;li&gt;Nginx looks at the hostname (SNI), routes to the correct backend&lt;/li&gt;
&lt;li&gt;Backend responds, nginx forwards response to client&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key insight: nginx uses the &lt;strong&gt;hostname&lt;/strong&gt; (Server Name Indication) to route traffic. All services share ports 80/443, but nginx routes based on which subdomain you’re requesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Why Run Nginx on Your Router?
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;You could run nginx anywhere - a Raspberry Pi, a VM, a Docker container. So why specifically on the router?&lt;/p&gt;

&lt;h3&gt;
  
  
  Elimination of Extra Hops
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Every network request already goes through your router. If nginx runs on a separate machine, you add an extra hop:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2crgm85bqrc0zm5dpg41.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2crgm85bqrc0zm5dpg41.png" alt="Routing_Diagram" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One less hop means lower latency, less routing rules, and one less point of failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Single Point of Configuration
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Your router already manages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DHCP (IP assignments)&lt;/li&gt;
&lt;li&gt;DNS (name resolution via dnsmasq)&lt;/li&gt;
&lt;li&gt;Firewall rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding reverse proxying to this list keeps all network configuration in one place. When you add a new service, you configure the DNS entry and nginx proxy in the same system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Always-On Guarantee
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Your router is the one device that’s always running. If it’s down, you have no network anyway. Running nginx on the router means your reverse proxy has the same uptime as your network itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resource Efficiency
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Modern routers have more than enough power for reverse proxying. Nginx is extremely lightweight - it was designed to handle thousands of concurrent connections on minimal hardware. My OpenWrt One barely notices the 25 proxy configurations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; total used free shared buff/cache available
Mem: 1011248 163144 241180 505676 606924 291924

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Less than 200MB used with nginx, dnsmasq, and all other services running.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Installing Nginx on OpenWrt
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;SSH into your router and install nginx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;opkg update
opkg install nginx-ssl

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;nginx-ssl&lt;/code&gt; package includes SSL/TLS support. Without it, you can only proxy HTTP.&lt;/p&gt;

&lt;p&gt;Enable and start the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/init.d/nginx enable
/etc/init.d/nginx start

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify it’s running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nginx -v
# nginx version: nginx/1.26.1 (x86_64-pc-linux-gnu)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Understanding OpenWrt’s UCI Configuration
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;OpenWrt uses &lt;strong&gt;UCI&lt;/strong&gt; (Unified Configuration Interface) to manage all system configuration, including nginx. Instead of editing nginx config files directly, you define settings through UCI and OpenWrt generates the actual nginx config at &lt;code&gt;/var/lib/nginx/uci.conf&lt;/code&gt; on each restart.&lt;/p&gt;

&lt;p&gt;Nginx configuration on OpenWrt has two parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Server blocks&lt;/strong&gt; (via UCI) - Define which subdomain to listen for and which SSL certificate to use&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location files&lt;/strong&gt; (plain nginx config) - Define where to proxy the traffic&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Part 1: Server Block (UCI)
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;A server block tells nginx: “When someone requests this subdomain, use this SSL certificate and look at this location file for routing rules.”&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uci set nginx.srv_grafana=server
uci set nginx.srv_grafana.uci_enable='true'
uci set nginx.srv_grafana.server_name='grafana.raspberrypi.home'
uci set nginx.srv_grafana.include='conf.d/grafana.locations'
uci set nginx.srv_grafana.ssl_certificate='/etc/nginx/ssl/wildcard.raspberrypi.home.crt'
uci set nginx.srv_grafana.ssl_certificate_key='/etc/nginx/ssl/wildcard.raspberrypi.home.key'
uci add_list nginx.srv_grafana.listen='443 ssl'
uci add_list nginx.srv_grafana.listen='[::]:443 ssl'
uci set nginx.srv_grafana.ssl_session_cache='shared:SSL:32k'
uci set nginx.srv_grafana.ssl_session_timeout='64m'
uci commit nginx
/etc/init.d/nginx restart

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key line is &lt;code&gt;server_name&lt;/code&gt; - this enables &lt;strong&gt;SNI (Server Name Indication)&lt;/strong&gt;. When a client connects to port 443 and says “I want grafana.raspberrypi.home”, nginx matches it to this server block.&lt;/p&gt;

&lt;h3&gt;
  
  
  Part 2: Location File
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;The location file tells nginx where to actually send the traffic. Create &lt;code&gt;/etc/nginx/conf.d/grafana.locations&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;location / {
 proxy_pass http://192.168.1.201:3000;
 proxy_http_version 1.1;
 proxy_set_header Upgrade $http_upgrade;
 proxy_set_header Connection "upgrade";
 proxy_set_header Host $host;
 proxy_set_header X-Real-IP $remote_addr;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_set_header X-Forwarded-Proto $scheme;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;proxy_pass&lt;/code&gt; - The backend server address and port&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Upgrade&lt;/code&gt; / &lt;code&gt;Connection&lt;/code&gt; - Enable WebSocket support&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;X-Real-IP&lt;/code&gt; / &lt;code&gt;X-Forwarded-For&lt;/code&gt; - Pass the client’s real IP to the backend&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;X-Forwarded-Proto&lt;/code&gt; - Tell the backend the original request was HTTPS&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Setting Up Wildcard SSL Certificates
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Instead of managing certificates for each subdomain, use a &lt;strong&gt;wildcard certificate&lt;/strong&gt; that covers &lt;code&gt;*.raspberrypi.home&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generating a Self-Signed Wildcard Certificate
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;On your router or any Linux machine with OpenSSL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Generate private key
openssl genrsa -out wildcard.raspberrypi.home.key 2048

# Generate certificate signing request
openssl req -new -key wildcard.raspberrypi.home.key \
 -out wildcard.raspberrypi.home.csr \
 -subj "/CN=*.raspberrypi.home"

# Generate self-signed certificate (valid for 10 years)
openssl x509 -req -days 3650 \
 -in wildcard.raspberrypi.home.csr \
 -signkey wildcard.raspberrypi.home.key \
 -out wildcard.raspberrypi.home.crt

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Place the files in &lt;code&gt;/etc/nginx/ssl/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir -p /etc/nginx/ssl
mv wildcard.raspberrypi.home.* /etc/nginx/ssl/
chmod 600 /etc/nginx/ssl/*.key

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Browser Trust
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Self-signed certificates will show browser warnings until you install them. Import &lt;code&gt;wildcard.raspberrypi.home.crt&lt;/code&gt; into your browser or OS trust store to make the warnings go away. On most systems, double-clicking the certificate file will open an import wizard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5np31z24wbkqxesjmbxn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5np31z24wbkqxesjmbxn.png" alt="Accessing Grafana via clean subdomain URL with HTTPS" width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Adding Your First Service
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Let’s add a complete example for Home Assistant:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create the Location File
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;/etc/nginx/conf.d/homeassistant.locations&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;location / {
 proxy_pass http://192.168.1.213:8123;
 proxy_http_version 1.1;
 proxy_set_header Upgrade $http_upgrade;
 proxy_set_header Connection "upgrade";
 proxy_set_header Host $host;
 proxy_set_header X-Real-IP $remote_addr;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_set_header X-Forwarded-Proto $scheme;

 # Home Assistant specific settings
 proxy_buffering off;
 proxy_request_buffering off;
 chunked_transfer_encoding on;
 tcp_nodelay on;
 proxy_read_timeout 3600s; # Long timeout for WebSocket connections
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Home Assistant uses WebSockets heavily, so we disable buffering and set a long read timeout.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Add UCI Configuration
&lt;/h3&gt;

&lt;p&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uci set nginx.srv_homeassistant=server
uci set nginx.srv_homeassistant.uci_enable='true'
uci set nginx.srv_homeassistant.server_name='homeassistant.raspberrypi.home'
uci set nginx.srv_homeassistant.include='conf.d/homeassistant.locations'
uci set nginx.srv_homeassistant.ssl_certificate='/etc/nginx/ssl/wildcard.raspberrypi.home.crt'
uci set nginx.srv_homeassistant.ssl_certificate_key='/etc/nginx/ssl/wildcard.raspberrypi.home.key'
uci add_list nginx.srv_homeassistant.listen='443 ssl'
uci add_list nginx.srv_homeassistant.listen='[::]:443 ssl'
uci set nginx.srv_homeassistant.ssl_session_cache='shared:SSL:32k'
uci set nginx.srv_homeassistant.ssl_session_timeout='64m'
uci commit nginx
/etc/init.d/nginx restart

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Verify
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Check nginx configuration is valid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nginx -t

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  8. DNS Configuration with dnsmasq
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;For subdomains to resolve to your router, configure dnsmasq (OpenWrt’s built-in DNS server).&lt;/p&gt;

&lt;h3&gt;
  
  
  Wildcard DNS Entry
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;The cleanest approach is a wildcard entry that routes all &lt;code&gt;*.raspberrypi.home&lt;/code&gt; to your router:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uci set dhcp.@dnsmasq[0].domain='raspberrypi.home'
uci add dhcp domain
uci set dhcp.@domain[-1].name='raspberrypi.home'
uci set dhcp.@domain[-1].ip='192.168.1.1'
uci commit dhcp
/etc/init.d/dnsmasq restart

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now any &lt;code&gt;*.raspberrypi.home&lt;/code&gt; request resolves to 192.168.1.1, where nginx handles routing based on the subdomain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing DNS Resolution
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;From any device on your network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nslookup grafana.raspberrypi.home
# Should return 192.168.1.1

nslookup anything.raspberrypi.home
# Also returns 192.168.1.1

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  9. Real Example: 25 Services Proxied Through One Router
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Here’s my actual setup - services running across Raspberry Pis, an N305 mini PC, and other devices, all proxied through the OpenWrt One. Every service accessible via clean subdomain URLs with HTTPS:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw01y0xk1ipgvx8sp4v7n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw01y0xk1ipgvx8sp4v7n.png" alt="Homer dashboard with all services accessible via subdomains" width="800" height="548"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Subdomain&lt;/th&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Homer (Dashboard)&lt;/td&gt;
&lt;td&gt;homer.raspberrypi.home&lt;/td&gt;
&lt;td&gt;192.168.1.201:8080&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grafana&lt;/td&gt;
&lt;td&gt;grafana.raspberrypi.home&lt;/td&gt;
&lt;td&gt;192.168.1.201:3000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prometheus&lt;/td&gt;
&lt;td&gt;prometheus.raspberrypi.home&lt;/td&gt;
&lt;td&gt;192.168.1.201:9090&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home Assistant&lt;/td&gt;
&lt;td&gt;homeassistant.raspberrypi.home&lt;/td&gt;
&lt;td&gt;192.168.1.213:8123&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jellyfin&lt;/td&gt;
&lt;td&gt;jellyfin.raspberrypi.home&lt;/td&gt;
&lt;td&gt;192.168.1.201:8096&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portainer&lt;/td&gt;
&lt;td&gt;portainer.raspberrypi.home&lt;/td&gt;
&lt;td&gt;192.168.1.201:9000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bitwarden&lt;/td&gt;
&lt;td&gt;bitwarden.raspberrypi.home&lt;/td&gt;
&lt;td&gt;192.168.1.201:8081&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qBittorrent&lt;/td&gt;
&lt;td&gt;qbittorrent.raspberrypi.home&lt;/td&gt;
&lt;td&gt;192.168.1.201:8090&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Komga&lt;/td&gt;
&lt;td&gt;komga.raspberrypi.home&lt;/td&gt;
&lt;td&gt;192.168.1.201:8082&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uptime Kuma&lt;/td&gt;
&lt;td&gt;uptimekuma.raspberrypi.home&lt;/td&gt;
&lt;td&gt;192.168.1.201:3001&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Syncthing&lt;/td&gt;
&lt;td&gt;syncthing.raspberrypi.home&lt;/td&gt;
&lt;td&gt;192.168.1.201:8384&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guacamole&lt;/td&gt;
&lt;td&gt;guacamole.raspberrypi.home&lt;/td&gt;
&lt;td&gt;192.168.1.201:8083&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PiKVM&lt;/td&gt;
&lt;td&gt;pikvm.raspberrypi.home&lt;/td&gt;
&lt;td&gt;192.168.1.100:443&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OctoPrint&lt;/td&gt;
&lt;td&gt;octopi.raspberrypi.home&lt;/td&gt;
&lt;td&gt;192.168.1.101:80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;…&lt;/td&gt;
&lt;td&gt;…&lt;/td&gt;
&lt;td&gt;…&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every service is accessed through &lt;code&gt;https://servicename.raspberrypi.home&lt;/code&gt; on port 443. You don’t need to remember different port numbers - nginx reads the subdomain from the URL and forwards your request to the correct backend service internally.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Tips and Considerations
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Logging
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;OpenWrt centralizes all logs through &lt;code&gt;logread&lt;/code&gt;. To view nginx logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;logread | grep nginx

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, access logging is disabled (&lt;code&gt;access_log off&lt;/code&gt;) since router storage is limited. Error logs still go to the system log and are accessible via &lt;code&gt;logread&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backup Your Configuration
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;UCI configurations live in &lt;code&gt;/etc/config/&lt;/code&gt;. Location files are in &lt;code&gt;/etc/nginx/conf.d/&lt;/code&gt;. Back these up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Create backup
tar -czf nginx-backup.tar.gz /etc/config/nginx /etc/nginx/conf.d/ /etc/nginx/ssl/

# Restore
tar -xzf nginx-backup.tar.gz -C /

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Service-Specific Configuration
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Some services need to know they’re behind a reverse proxy. Nginx alone isn’t enough - you need to configure the service itself. For example, Grafana requires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;environment:
 - GF_SERVER_ROOT_URL=https://grafana.yourdomain.home

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this, Grafana fails to load its assets through the proxy. Check your service’s documentation for reverse proxy settings if things don’t work after setting up nginx.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s Next
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;This post covered local access to your homelab services. In an upcoming post, I’ll cover remote access - setting up WireGuard on OpenWrt to securely reach your services from anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Running nginx on your OpenWrt router leverages hardware you already have running 24/7. It eliminates network hops, centralizes configuration with DNS, and provides a clean subdomain-based access pattern for all your services.&lt;/p&gt;

&lt;p&gt;The combination of OpenWrt’s UCI system and nginx’s flexibility creates a maintainable setup that scales from a handful of services to dozens. My 25-service configuration runs on minimal resources and has been rock solid.&lt;/p&gt;

&lt;p&gt;Start with one or two services, get comfortable with the UCI workflow, and expand from there. Your router is more capable than you might think.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write weekly about homelabs, monitoring, and DevOps. If you found this helpful, check out my other posts or subscribe on &lt;a href="https://dev.to/tsukiyo"&gt;Dev.to&lt;/a&gt; for more practical guides like this one.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>nginx</category>
      <category>homelab</category>
      <category>networking</category>
    </item>
    <item>
      <title>End-to-End Monitoring Explained for Homelabs: Prometheus, Grafana &amp; Alertmanager</title>
      <dc:creator>Natsuki</dc:creator>
      <pubDate>Fri, 09 Jan 2026 20:34:29 +0000</pubDate>
      <link>https://dev.to/tsukiyo/end-to-end-monitoring-explained-for-homelabs-prometheus-grafana-alertmanager-2g3k</link>
      <guid>https://dev.to/tsukiyo/end-to-end-monitoring-explained-for-homelabs-prometheus-grafana-alertmanager-2g3k</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;When you’re running a homelab with dozens of containers and services, things will eventually break. The question isn’t &lt;em&gt;if&lt;/em&gt; something will fail, but &lt;em&gt;when&lt;/em&gt; - and whether you’ll know about it before your users do.&lt;/p&gt;

&lt;p&gt;This post walks through building a production-grade monitoring stack using Prometheus + Grafana that monitors 37 containers across 2 hosts, with automatic email alerting and comprehensive dashboards. You’ll get visibility into CPU, memory, disk, network, container metrics, and even ZFS storage - all with 30 days of historical data.&lt;/p&gt;

&lt;p&gt;Full Configuration is available on &lt;a href="https://github.com/tsuki-yo/homelab-monitoring" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Clone and customize for your homelab.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📦 This monitoring stack runs on my DeskPi 12U homelab in a 10sqm Tokyo apartment. See the full setup: &lt;a href="https://dev.to/tsukiyo/small-but-mighty-homelab-deskpi-12u-running-20-services-4l7f"&gt;Small But Mighty Homelab: DeskPi 12U Running 20+ Services&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Contents:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Why Monitor Your Homelab?&lt;/li&gt;
&lt;li&gt;The Stack Overview&lt;/li&gt;
&lt;li&gt;Architecture&lt;/li&gt;
&lt;li&gt;Docker Compose Setup&lt;/li&gt;
&lt;li&gt;Prometheus Configuration&lt;/li&gt;
&lt;li&gt;Node Exporter for Host Metrics&lt;/li&gt;
&lt;li&gt;cAdvisor for Container Metrics&lt;/li&gt;
&lt;li&gt;Grafana Setup &amp;amp; Data Source&lt;/li&gt;
&lt;li&gt;Building Dashboards&lt;/li&gt;
&lt;li&gt;Alert Rules&lt;/li&gt;
&lt;li&gt;Alertmanager &amp;amp; Notifications&lt;/li&gt;
&lt;li&gt;Tips &amp;amp; Lessons Learned&lt;/li&gt;
&lt;li&gt;What’s Next?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1. Why Monitor Your Homelab?
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;A proper monitoring stack gives you three critical capabilities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Visibility&lt;/strong&gt; : Know what’s happening right now across all your services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerting&lt;/strong&gt; : Get notified when things go wrong, before they become critical&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugging&lt;/strong&gt; : Historical data to troubleshoot issues and understand trends&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  2. The Stack Overview
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;I chose the classic observability stack that’s proven itself in production environments:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Node Exporter&lt;/strong&gt; : Exposes system-level metrics (CPU, memory, disk, network)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cAdvisor&lt;/strong&gt; : Collects container resource usage and performance metrics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prometheus&lt;/strong&gt; : Time-series database that scrapes and stores metrics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grafana&lt;/strong&gt; : Visualization platform for creating dashboards&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alertmanager&lt;/strong&gt; : Handles alert routing and notifications&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why this stack? It’s open-source, widely adopted, handles homelab scale easily, and doesn’t require expensive licensing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1095he4qvazt92oi5a6i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1095he4qvazt92oi5a6i.png" alt="overview diagram" width="800" height="529"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;My homelab runs on &lt;strong&gt;Proxmox&lt;/strong&gt; - a bare-metal hypervisor that lets me run multiple isolated workloads on the same hardware without the overhead of full VMs. I use &lt;strong&gt;LXC containers&lt;/strong&gt; as lightweight virtual environments, and run &lt;strong&gt;Docker inside the LXC containers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The monitoring stack (Prometheus, Grafana, Alertmanager, cAdvisor) runs in Docker containers inside an LXC. Node Exporter runs natively on each Proxmox host (outside the LXC) because running it inside Docker-in-LXC reports incorrect memory metrics due to nested containerization.&lt;/p&gt;

&lt;p&gt;My setup monitors two hosts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Raspberry Pi 5&lt;/strong&gt; (192.168.1.201): Running the monitoring stack itself&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;N305 Server&lt;/strong&gt; (192.168.1.50): Main server running 29 containers (Immich, Bitwarden, Jellyfin, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6k2t3qvivj1nu9prrofg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6k2t3qvivj1nu9prrofg.png" alt="architecture diagram" width="800" height="257"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Docker Compose Setup
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;The monitoring stack runs entirely in Docker on the Raspberry Pi 5. Here’s the complete &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;networks:
 internal:
 driver: bridge

services:
 prometheus:
 container_name: monitoring-prometheus
 image: prom/prometheus:latest
 hostname: rpi-prometheus
 restart: unless-stopped
 user: "nobody"
 networks:
 - internal
 ports:
 - "9090:9090"
 command:
 - '--config.file=/etc/prometheus/prometheus.yml'
 - '--storage.tsdb.path=/prometheus'
 - '--storage.tsdb.retention.time=30d'
 - '--web.enable-admin-api'
 volumes:
 - /home/ubuntu/docker/prometheus/config:/etc/prometheus
 - /home/ubuntu/docker/prometheus/data:/prometheus
 depends_on:
 - cadvisor
 - alertmanager
 links:
 - cadvisor:cadvisor
 - alertmanager:alertmanager

 grafana:
 container_name: monitoring-grafana
 image: grafana/grafana:latest
 hostname: rpi-grafana
 restart: unless-stopped
 user: "472"
 networks:
 - internal
 ports:
 - "3000:3000"
 volumes:
 - /home/ubuntu/docker/grafana/data:/var/lib/grafana
 - /home/ubuntu/docker/grafana/provisioning:/etc/grafana/provisioning
 depends_on:
 - prometheus

 alertmanager:
 container_name: monitoring-alertmanager
 image: prom/alertmanager:latest
 hostname: rpi-alertmanager
 restart: unless-stopped
 networks:
 - internal
 ports:
 - "9093:9093"
 volumes:
 - /home/ubuntu/docker/alertmanager:/etc/alertmanager
 command:
 - '--config.file=/etc/alertmanager/alertmanager.yml'
 - '--storage.path=/alertmanager'

 cadvisor:
 container_name: monitoring-cadvisor
 image: gcr.io/cadvisor/cadvisor:v0.49.1
 hostname: rpi-cadvisor
 restart: unless-stopped
 privileged: true
 networks:
 - internal
 expose:
 - 8080
 command:
 - '-housekeeping_interval=15s'
 - '-docker_only=true'
 - '-store_container_labels=false'
 devices:
 - /dev/kmsg
 volumes:
 - /:/rootfs:ro
 - /var/run:/var/run:rw
 - /sys:/sys:ro
 - /var/lib/docker/:/var/lib/docker:ro
 - /dev/disk/:/dev/disk:ro
 - /etc/machine-id:/etc/machine-id:ro

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key configuration details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;30-day retention&lt;/strong&gt; : Prometheus keeps 30 days of metrics data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;15-second housekeeping&lt;/strong&gt; : cAdvisor updates container metrics every 15 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privileged cAdvisor&lt;/strong&gt; : Required to read container metrics from the host&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal network&lt;/strong&gt; : Services communicate via Docker network, only exposing necessary ports&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Prometheus Configuration
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Prometheus is a time-series database that stores metrics as data points with timestamps. It works by periodically “scraping” (pulling) metrics from configured endpoints over HTTP. Each scrape collects current metric values and stores them with a timestamp, allowing you to query historical trends.&lt;/p&gt;

&lt;p&gt;The key concept: &lt;strong&gt;Prometheus pulls metrics&lt;/strong&gt; - it doesn’t wait for services to push data. This means your services need to expose a &lt;code&gt;/metrics&lt;/code&gt; endpoint that Prometheus can scrape.&lt;/p&gt;

&lt;p&gt;Prometheus needs to know what to scrape. Here’s my &lt;code&gt;prometheus.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;global:
 scrape_interval: 15s
 evaluation_interval: 15s

alerting:
 alertmanagers:
 - static_configs:
 - targets:
 - alertmanager:9093

rule_files:
 - "alerts/*.yml"

scrape_configs:
 # Prometheus itself
 - job_name: 'prometheus'
 scrape_interval: 5s
 static_configs:
 - targets: ['localhost:9090']

 # Raspberry Pi 5 - Node Exporter
 - job_name: 'monitoring-host-node'
 scrape_interval: 15s
 static_configs:
 - targets: ['192.168.1.201:9100']
 labels:
 host: 'monitoring-host'
 instance_name: 'rpi-monitoring'

 # Raspberry Pi 5 - cAdvisor
 - job_name: 'monitoring-host-cadvisor'
 scrape_interval: 15s
 static_configs:
 - targets: ['cadvisor:8080']
 labels:
 host: 'monitoring-host'
 instance_name: 'rpi-monitoring'

 # N305 - Node Exporter
 - job_name: 'n305-node'
 scrape_interval: 15s
 static_configs:
 - targets: ['192.168.1.50:9100']
 labels:
 host: 'n305'
 instance_name: 'n305-server'

 # N305 - cAdvisor
 - job_name: 'n305-cadvisor'
 scrape_interval: 15s
 static_configs:
 - targets: ['192.168.1.50:8080']
 labels:
 host: 'n305'
 instance_name: 'n305-server'

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important notes&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Jobs&lt;/strong&gt; : A “job” groups related scrape targets together (e.g., &lt;code&gt;monitoring-host-node&lt;/code&gt;, &lt;code&gt;n305-cadvisor&lt;/code&gt;). Prometheus automatically adds a &lt;code&gt;job&lt;/code&gt; label to every metric, so you can filter by job in queries. I use separate jobs for Node Exporter vs cAdvisor metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom labels&lt;/strong&gt; : Each target gets &lt;code&gt;host&lt;/code&gt; and &lt;code&gt;instance_name&lt;/code&gt; labels for easier filtering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;15-second scrapes&lt;/strong&gt; : Balance between data granularity and resource usage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert rules&lt;/strong&gt; : Loaded from separate files in &lt;code&gt;alerts/&lt;/code&gt; directory&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Node Exporter for Host Metrics
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Node Exporter is a Prometheus exporter that exposes hardware and OS-level metrics from Linux systems. It runs as a service on each host and provides a &lt;code&gt;/metrics&lt;/code&gt; HTTP endpoint that Prometheus can scrape.&lt;/p&gt;

&lt;p&gt;Think of it as a bridge between your system’s kernel statistics (CPU, memory, disk, network) and Prometheus. It reads data from &lt;code&gt;/proc&lt;/code&gt;, &lt;code&gt;/sys&lt;/code&gt;, and other system sources, then formats it into Prometheus-compatible metrics.&lt;/p&gt;

&lt;p&gt;Node Exporter runs natively (not in Docker) on both hosts to collect system-level metrics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;

&lt;p&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Install Node Exporter
wget https://github.com/prometheus/node_exporter/releases/download/v1.7.0/node_exporter-1.7.0.linux-amd64.tar.gz
tar xvfz node_exporter-1.7.0.linux-amd64.tar.gz
sudo cp node_exporter-1.7.0.linux-amd64/node_exporter /usr/local/bin/

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Systemd Service
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;/etc/systemd/system/node_exporter.service&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Unit]
Description=Node Exporter
After=network.target

[Service]
Type=simple
User=node_exporter
ExecStart=/usr/local/bin/node_exporter

[Install]
WantedBy=multi-user.target


sudo useradd -rs /bin/false node_exporter
sudo systemctl daemon-reload
sudo systemctl enable node_exporter
sudo systemctl start node_exporter

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why native instead of Docker?&lt;/strong&gt; I initially ran Node Exporter in Docker within LXC containers, but it reported incorrect memory usage (0.1% instead of actual 40%). Running it natively on the host gives accurate metrics.&lt;/p&gt;

&lt;p&gt;Node Exporter exposes hundreds of metrics at &lt;code&gt;:9100/metrics&lt;/code&gt;, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU usage per core and mode (user, system, idle, iowait)&lt;/li&gt;
&lt;li&gt;Memory (total, available, cached, buffers, swap)&lt;/li&gt;
&lt;li&gt;Disk usage and I/O statistics&lt;/li&gt;
&lt;li&gt;Network interface traffic and errors&lt;/li&gt;
&lt;li&gt;Load averages&lt;/li&gt;
&lt;li&gt;Filesystem usage (including ZFS pools!)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3r045ed78dxg0d1rosf2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3r045ed78dxg0d1rosf2.png" alt="Grafana All Nodes Dashboard" width="800" height="399"&gt;&lt;/a&gt;&lt;em&gt;The “All Nodes” dashboard displaying N305 system metrics&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. cAdvisor for Container Metrics
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;cAdvisor (Container Advisor) is Google’s open-source container monitoring tool. It automatically discovers all containers on a host and collects resource usage metrics: CPU, memory, network, and disk I/O per container.&lt;/p&gt;

&lt;p&gt;Unlike Node Exporter which monitors the host system, cAdvisor specifically monitors containerized applications. It understands Docker’s resource limits (enforced via Linux cgroups - the kernel feature that isolates container resources) and can show both usage and limits for each container.&lt;/p&gt;

&lt;p&gt;cAdvisor runs in Docker and monitors all other containers on the same host.&lt;/p&gt;

&lt;p&gt;The key configuration flags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;command:
 - '-housekeeping_interval=15s' # Update metrics every 15s
 - '-docker_only=true' # Only monitor Docker containers
 - '-store_container_labels=false' # Don't store all labels (reduces cardinality)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why privileged mode?&lt;/strong&gt; cAdvisor needs access to the host’s cgroups (Linux kernel’s resource isolation mechanism) to read container resource usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;privileged: true
devices:
 - /dev/kmsg
volumes:
 - /:/rootfs:ro
 - /var/run:/var/run:rw
 - /sys:/sys:ro
 - /var/lib/docker/:/var/lib/docker:ro

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;cAdvisor provides metrics like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Container CPU usage (total and per-core)&lt;/li&gt;
&lt;li&gt;Container memory usage and limits&lt;/li&gt;
&lt;li&gt;Network I/O per container&lt;/li&gt;
&lt;li&gt;Disk I/O per container&lt;/li&gt;
&lt;li&gt;Container restart counts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkmnvpb6k0nyaogpf9okk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkmnvpb6k0nyaogpf9okk.png" alt="cAdvisor Dashboard" width="800" height="424"&gt;&lt;/a&gt;&lt;em&gt;cAdvisor dashboard displaying container metrics across all monitored hosts&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Grafana Setup &amp;amp; Data Source
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Grafana is the visualization tool that turns Prometheus metrics into beautiful dashboards and graphs. While Prometheus stores the raw time-series data, Grafana connects to it as a “data source” and uses PromQL (Prometheus Query Language) to query and visualize metrics. You could query Prometheus directly, but Grafana makes it visual and user-friendly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initial Setup
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;After starting Grafana, log in at &lt;code&gt;http://192.168.1.201:3000&lt;/code&gt; (default credentials: admin/admin).&lt;/p&gt;

&lt;h3&gt;
  
  
  Provisioning Prometheus Data Source
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Instead of manually adding the data source, provision it automatically with a YAML file in &lt;code&gt;/home/ubuntu/docker/grafana/provisioning/datasources/prometheus.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: 1

datasources:
 - name: Prometheus
 type: prometheus
 access: proxy
 url: http://prometheus:9090
 isDefault: true
 editable: false
 uid: PBFA97CFB590B2093

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt; : The &lt;code&gt;uid&lt;/code&gt; must match what you use in dashboard JSON files. I learned this the hard way when my dashboards showed “N/A” everywhere because they had a hardcoded UID that didn’t match my actual Prometheus datasource!&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Building Dashboards
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Dashboards transform raw Prometheus metrics into visual panels you can actually understand at a glance. Instead of querying Prometheus manually with PromQL, you build panels (gauges, graphs, stats) that auto-update and show trends over time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3r045ed78dxg0d1rosf2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3r045ed78dxg0d1rosf2.png" alt="Grafana All Nodes Dashboard" width="800" height="399"&gt;&lt;/a&gt;&lt;em&gt;The “All Nodes” dashboard displaying N305 system metrics&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The dashboard has 12 panels showing current status and historical trends:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Top row&lt;/strong&gt; : Colored gauges (CPU 31%, Memory 40.4%, Root FS 58.5%) and stat panels (Load 0.27, Uptime 1.49 days, 8 CPU cores)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Middle section&lt;/strong&gt; : Network traffic graphs for all interfaces, plus CPU and memory usage time-series&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bottom row&lt;/strong&gt; : ZFS storage showing 160 GiB used out of 2.3 TB (2.4% full)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can build this manually by clicking “Add Panel” in Grafana, or export/import dashboard JSON files for faster setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dashboard Variables
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Variables make your dashboard reusable across multiple hosts. Instead of hardcoding &lt;code&gt;instance="192.168.1.201:9100"&lt;/code&gt; in every query, you use &lt;code&gt;instance="$instance"&lt;/code&gt; and select which host to view from a dropdown.&lt;/p&gt;

&lt;p&gt;Create these variables in Dashboard settings &amp;gt; Variables:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Job selector&lt;/strong&gt; (which monitoring job to view):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name: job
Type: Query
Query: label_values(node_uname_info, job)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Instance selector&lt;/strong&gt; (which specific host):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name: instance
Type: Query
Query: label_values(node_uname_info{job="$job"}, instance)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can switch between Raspberry Pi 5 and N305 using dropdowns at the top of the dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key PromQL Queries
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Here are the queries powering each panel type. PromQL is Prometheus’s query language - it looks intimidating at first, but once you understand a few patterns, it’s straightforward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gauge Panels&lt;/strong&gt; (current value with color thresholds):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# CPU Usage (%)
100 - (avg(rate(node_cpu_seconds_total{mode="idle",instance="$instance",job="$job"}[5m])) * 100)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This calculates CPU usage by measuring how much time the CPU is &lt;em&gt;not&lt;/em&gt; idle over a 5-minute window. The &lt;code&gt;rate()&lt;/code&gt; function converts cumulative counters into per-second rates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Memory Usage (%)
100 * (1 - ((node_memory_MemAvailable_bytes{instance="$instance",job="$job"}) / node_memory_MemTotal_bytes{instance="$instance",job="$job"}))

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Uses &lt;code&gt;MemAvailable&lt;/code&gt; instead of &lt;code&gt;MemFree&lt;/code&gt; because Linux caches unused memory. &lt;code&gt;MemAvailable&lt;/code&gt; accounts for reclaimable cache, giving you the real available memory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Root Disk Usage (%)
100 - ((node_filesystem_avail_bytes{instance="$instance",job="$job",mountpoint="/"} / node_filesystem_size_bytes{instance="$instance",job="$job",mountpoint="/"}) * 100)

# ZFS Storage (%)
(1 - (node_filesystem_avail_bytes{instance="$instance",job="$job",fstype="zfs",mountpoint="/storage/media"} / node_filesystem_size_bytes{instance="$instance",job="$job",fstype="zfs",mountpoint="/storage/media"})) * 100

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Stat Panels&lt;/strong&gt; (single number display):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Load Average (1 minute)
node_load1{instance="$instance",job="$job"}

# System Uptime (seconds)
node_time_seconds{instance="$instance",job="$job"} - node_boot_time_seconds{instance="$instance",job="$job"}

# CPU Core Count
count(count(node_cpu_seconds_total{instance="$instance",job="$job"}) by (cpu))

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Time-Series Graphs&lt;/strong&gt; (trends over time):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# CPU Usage History
100 - (avg(rate(node_cpu_seconds_total{mode="idle",instance="$instance",job="$job"}[5m])) * 100)

# Memory Usage History
100 * (1 - ((node_memory_MemAvailable_bytes{instance="$instance",job="$job"}) / node_memory_MemTotal_bytes{instance="$instance",job="$job"}))

# Network Receive Rate (per device)
rate(node_network_receive_bytes_total{instance="$instance",job="$job",device!="lo"}[5m])

# Network Transmit Rate (per device)
rate(node_network_transmit_bytes_total{instance="$instance",job="$job",device!="lo"}[5m])

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;device!="lo"&lt;/code&gt; filter excludes the loopback interface since you only care about physical network traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Alert Rules
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Prometheus continuously evaluates alert rules defined in YAML files. When a condition is met for the specified duration (&lt;code&gt;for: 5m&lt;/code&gt;), Prometheus fires the alert and sends it to Alertmanager for routing and notification.&lt;/p&gt;

&lt;p&gt;Alert rules are defined in separate YAML files loaded by Prometheus.&lt;/p&gt;

&lt;h3&gt;
  
  
  System Alerts
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/home/ubuntu/docker/prometheus/config/alerts/system-alerts.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;groups:
 - name: system-alerts
 rules:
 # Disk space critical (&amp;lt;10% free)
 - alert: DiskSpaceLow
 expr: (node_filesystem_avail_bytes{fstype=~"ext4|xfs"} / node_filesystem_size_bytes{fstype=~"ext4|xfs"}) * 100 &amp;lt; 10
 for: 5m
 labels:
 severity: critical
 annotations:
 summary: "Disk space low on {{ $labels.instance }}"
 description: "Disk {{ $labels.mountpoint }} on {{ $labels.instance }} has less than 10% free space ({{ $value | printf \"%.1f\" }}% free)"

 # High memory usage (&amp;gt;90%)
 - alert: HighMemoryUsage
 expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 &amp;gt; 90
 for: 5m
 labels:
 severity: warning
 annotations:
 summary: "High memory usage on {{ $labels.instance }}"
 description: "Memory usage on {{ $labels.instance }} is above 90% (current: {{ $value | printf \"%.1f\" }}%)"

 # High CPU usage (&amp;gt;90% for 10 minutes)
 - alert: HighCPUUsage
 expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) &amp;gt; 90
 for: 10m
 labels:
 severity: warning
 annotations:
 summary: "High CPU usage on {{ $labels.instance }}"
 description: "CPU usage on {{ $labels.instance }} is above 90% for 10 minutes (current: {{ $value | printf \"%.1f\" }}%)"

 # Host down
 - alert: HostDown
 expr: up{job=~".*node.*"} == 0
 for: 2m
 labels:
 severity: critical
 annotations:
 summary: "Host {{ $labels.instance }} is down"
 description: "Node exporter on {{ $labels.instance }} has been unreachable for more than 2 minutes"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Container Alerts
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/home/ubuntu/docker/prometheus/config/alerts/container-alerts.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;groups:
 - name: container-alerts
 rules:
 # Container down
 - alert: ContainerDown
 expr: absent(container_last_seen{name=~".+"}) == 1
 for: 2m
 labels:
 severity: critical
 annotations:
 summary: "Container {{ $labels.name }} is down"
 description: "Container {{ $labels.name }} on {{ $labels.host }} has been down for more than 2 minutes"

 # Container high memory (&amp;gt;8GB)
 - alert: ContainerHighMemory
 expr: container_memory_usage_bytes{name=~".+"} &amp;gt; 8589934592
 for: 5m
 labels:
 severity: warning
 annotations:
 summary: "Container {{ $labels.name }} high memory"
 description: "Container {{ $labels.name }} on {{ $labels.host }} memory usage is above 8GB (current: {{ $value | humanize1024 }})"

 # Critical service down (Bitwarden, Immich)
 - alert: CriticalServiceDown
 expr: absent(container_last_seen{name=~"bitwarden|immich-server"})
 for: 1m
 labels:
 severity: critical
 annotations:
 summary: "Critical service {{ $labels.name }} is down"
 description: "Critical service {{ $labels.name }} has been down for more than 1 minute - immediate attention required"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  11. Alertmanager &amp;amp; Notifications
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Alertmanager receives alerts from Prometheus and handles routing, grouping, deduplication, and notification delivery. It allows complex routing logic - for example, sending critical alerts via SMS and warnings via email, without cluttering your Prometheus alert definitions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Up Gmail Notifications
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxg4jjp3apk2ths6bxm8p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxg4jjp3apk2ths6bxm8p.png" alt="Alert Email Example" width="800" height="284"&gt;&lt;/a&gt;&lt;em&gt;Email notification showing alert details with severity level, affected instance, and resolution instructions&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Before configuring Alertmanager, you need a Gmail App Password. This takes less than 2 minutes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://myaccount.google.com/apppasswords" rel="noopener noreferrer"&gt;https://myaccount.google.com/apppasswords&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sign in to your Google account&lt;/li&gt;
&lt;li&gt;Enter “Alertmanager” as the app name&lt;/li&gt;
&lt;li&gt;Click “Create”&lt;/li&gt;
&lt;li&gt;Copy the 16-character password (you’ll need it for the config below)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Alertmanager Configuration
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Configuration file: &lt;code&gt;/home/ubuntu/docker/alertmanager/alertmanager.yml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;global:
 resolve_timeout: 5m
 smtp_smarthost: smtp.gmail.com:587
 smtp_from: your-email@gmail.com
 smtp_auth_username: your-email@gmail.com
 smtp_auth_password: "your-app-password" # Use App Password from step 5, not your regular Gmail password. More secure and works with 2FA.
 smtp_require_tls: true

route:
 group_by: [alertname, severity]
 group_wait: 30s
 group_interval: 5m
 repeat_interval: 4h
 receiver: email
 routes:
 - match:
 severity: critical
 receiver: email-critical
 repeat_interval: 1h

receivers:
 - name: email
 email_configs:
 - to: your-email@gmail.com
 send_resolved: true
 headers:
 subject: "[{{ .Status | toUpper }}] {{ .GroupLabels.alertname }}"

 - name: email-critical
 email_configs:
 - to: your-email@gmail.com
 send_resolved: true
 headers:
 subject: "[CRITICAL] {{ .GroupLabels.alertname }}"

# Prevent warning alerts when critical alerts are firing
inhibit_rules:
 - source_match:
 severity: critical
 target_match:
 severity: warning
 equal: [alertname, instance]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key features&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multiple notification channels&lt;/strong&gt; : Supports email (Gmail, Outlook), Slack, Discord, PagerDuty, webhooks, and more. This guide uses Gmail SMTP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Severity-based routing&lt;/strong&gt; : Critical alerts repeat every hour, warnings every 4 hours&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert grouping&lt;/strong&gt; : Multiple alerts are grouped by name and severity to reduce noise&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inhibition&lt;/strong&gt; : Critical alerts suppress related warning alerts&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  12. Tips &amp;amp; Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Run Node Exporter Natively in LXC
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;If you’re running Prometheus in an LXC container, run Node Exporter natively on the host, not in Docker. Docker-in-LXC can report incorrect memory metrics because of the layered containerization.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use Provisioned Datasources
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Don’t manually configure Grafana datasources - provision them with YAML files. This makes your setup reproducible and ensures the datasource UID is consistent.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Dashboard UID Mismatches Will Haunt You
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;When importing dashboards, make sure the &lt;code&gt;datasource.uid&lt;/code&gt; in panel queries matches your actual Prometheus datasource UID. I spent way too long troubleshooting “N/A” values before realizing my dashboard had a hardcoded UID that didn’t exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Start Conservative with Alerts
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;It’s tempting to set aggressive thresholds, but you’ll end up with alert fatigue. Start conservative (90% disk usage, 10-minute CPU sustained) and tighten based on actual incidents.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Label Your Scrape Targets
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Add custom labels like &lt;code&gt;host&lt;/code&gt; and &lt;code&gt;instance_name&lt;/code&gt; to your scrape configs. This makes filtering and debugging much easier in Grafana and alert rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. What’s Next?
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;This monitoring setup covers infrastructure and container metrics, but there’s room for improvement:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Blackbox Exporter&lt;/strong&gt; : Monitor external endpoints (Is Bitwarden responding? Is Immich up?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres Exporter&lt;/strong&gt; : Database metrics for Immich, Plausible, n8n&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loki + Promtail&lt;/strong&gt; : Centralized log aggregation for debugging&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ZFS Health Monitoring&lt;/strong&gt; : Alert on scrub errors and pool degradation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSL Certificate Expiry&lt;/strong&gt; : Get warned before Let’s Encrypt certs expire&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SLI/SLO Tracking&lt;/strong&gt; : Service Level Indicators (response time, uptime %) and Objectives (target 99.9% uptime over certain period) for production-grade reliability engineering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Exporters&lt;/strong&gt; : Build exporters for applications that don’t expose metrics&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;This monitoring stack gives you complete visibility into your homelab - 37 containers across 2 hosts, all monitored from a single dashboard with automatic email alerts. The setup works whether you’re running on Proxmox, bare metal, or cloud VMs.&lt;/p&gt;

&lt;p&gt;The investment is worth it: you’ll catch issues before they escalate, learn production-grade observability skills, and sleep better knowing your homelab is monitored. The initial 4-hour setup pays for itself the first time it alerts you to a disk filling up or a service crashing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;write weekly about homelabs, monitoring, and DevOps. If you found this helpful, check out my other posts or subscribe on &lt;a href="https://dev.to/tsukiyo"&gt;Dev.to&lt;/a&gt; for more practical guides like this one.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>monitoring</category>
      <category>docker</category>
      <category>homelab</category>
    </item>
    <item>
      <title>Small But Mighty Homelab: DeskPi 12U Running 20+ Services</title>
      <dc:creator>Natsuki</dc:creator>
      <pubDate>Fri, 02 Jan 2026 11:58:54 +0000</pubDate>
      <link>https://dev.to/tsukiyo/small-but-mighty-homelab-deskpi-12u-running-20-services-4l7f</link>
      <guid>https://dev.to/tsukiyo/small-but-mighty-homelab-deskpi-12u-running-20-services-4l7f</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;You don't need a server room to run a powerful homelab. With just a few host machines - a couple of Raspberry Pis, a mini PC, and a Jetson - you can run 20+ services: media streaming, photo backup, password management, smart home automation, monitoring, and even a local AI voice assistant. All in a compact, elegant setup that fits in a corner of a tiny apartment.&lt;/p&gt;

&lt;p&gt;This post walks through my homelab architecture - how I got here, how it's organized, and what it can do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contents:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Evolution&lt;/li&gt;
&lt;li&gt;Architecture Overview&lt;/li&gt;
&lt;li&gt;Physical Foundation&lt;/li&gt;
&lt;li&gt;Network Infrastructure&lt;/li&gt;
&lt;li&gt;Compute Hosts &amp;amp; Devices&lt;/li&gt;
&lt;li&gt;Services &amp;amp; Applications&lt;/li&gt;
&lt;li&gt;Cost Breakdown&lt;/li&gt;
&lt;li&gt;Lessons Learned&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1. Evolution
&lt;/h2&gt;

&lt;p&gt;Living in a 10 sqm studio apartment in Tokyo, space is extremely limited. Every square centimeter counts.&lt;/p&gt;

&lt;p&gt;My homelab started simple: a metal rack with a Raspberry Pi 4 and a hard disk running Samba NAS. As my needs grew, so did the setup.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ftsuki-yo.github.io%2FPiBlog%2Fimages%2Fmetal_rack_server.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ftsuki-yo.github.io%2FPiBlog%2Fimages%2Fmetal_rack_server.jpg" alt="Initial metal rack setup" width="800" height="1066"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I moved to a TV rack as services expanded - Home Assistant, monitoring, Docker services. It worked, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cluttered mess of cables&lt;/li&gt;
&lt;li&gt;Ate up precious floor space&lt;/li&gt;
&lt;li&gt;Collected dust like crazy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ftsuki-yo.github.io%2FPiBlog%2Fimages%2FTV_rack_server.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ftsuki-yo.github.io%2FPiBlog%2Fimages%2FTV_rack_server.jpg" alt="TV rack server setup" width="800" height="602"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The solution? Graduating to a DeskPi 12U rack - vertical consolidation for small spaces, cleaner look, and better dust management.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Architecture Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.natsuki-cloud.dev%2Fimages%2FHomelab_Architecture.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.natsuki-cloud.dev%2Fimages%2FHomelab_Architecture.png" alt="Homelab Architecture" width="800" height="742"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Physical Foundation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The DeskPi 12U Rack Layout
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ftsuki-yo.github.io%2FPiBlog%2Fimages%2FPXL_20251205_053427791.MP~2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ftsuki-yo.github.io%2FPiBlog%2Fimages%2FPXL_20251205_053427791.MP~2.jpg" alt="DeskPi 12U Rack" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The DeskPi 12U rack is the physical foundation. It's visually pleasant and prevents dust buildup - a huge improvement over the open TV rack.&lt;/p&gt;

&lt;h3&gt;
  
  
  Power Management and Cooling
&lt;/h3&gt;

&lt;p&gt;Power comes from a 650W TUF Gaming power supply - repurposed from my old main PC. Plenty of headroom for the entire rack.&lt;/p&gt;

&lt;p&gt;For cooling, I installed 2x 12cm fans at the bottom of the rack, providing airflow from bottom to top. This keeps everything running cool without being too noisy.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Network Infrastructure
&lt;/h2&gt;

&lt;h3&gt;
  
  
  OpenWrt ONE Router
&lt;/h3&gt;

&lt;p&gt;The network starts with an OpenWrt ONE router - an open-source, hacker-friendly device that handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Routing, firewall, and DHCP&lt;/li&gt;
&lt;li&gt;Nginx reverse proxy for local service names (e.g., &lt;code&gt;jellyfin.local&lt;/code&gt; instead of &lt;code&gt;192.168.1.50:8096&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;WireGuard VPN for secure remote access via port forwarding&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  KP-9000-9XHP-X-AC Managed Switch
&lt;/h3&gt;

&lt;p&gt;The PoE managed switch serves dual purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Network switching for all devices in the rack&lt;/li&gt;
&lt;li&gt;PoE power delivery to both Raspberry Pis - eliminating two power adapters and reducing cable clutter&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Compute Hosts &amp;amp; Devices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Raspberry Pi 4 (PoE-powered)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Role:&lt;/strong&gt; Smart home automation&lt;/li&gt;
&lt;li&gt;Powered via PoE HAT - one less cable to manage&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Raspberry Pi 5 (PoE-powered)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Role:&lt;/strong&gt; Monitoring and observability&lt;/li&gt;
&lt;li&gt;Powered via PoE HAT&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Intel N305 Mini PC
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Role:&lt;/strong&gt; Main Docker host (Proxmox + LXC container)&lt;/li&gt;
&lt;li&gt;Intel i3-N305 (12th gen)&lt;/li&gt;
&lt;li&gt;4x Intel i226-V 2.5G NICs&lt;/li&gt;
&lt;li&gt;2x NVMe slots, 6x SATA 3.0 bays&lt;/li&gt;
&lt;li&gt;DDR5 RAM (32GB), PCIe x1, Type-C&lt;/li&gt;
&lt;li&gt;1TB NVMe + 2x 8TB Seagate IronWolf (ZFS mirror)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why the N305 over other options?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I considered Synology/QNAP NAS devices, used enterprise mini PCs, and Intel NUCs. The N305 NAS board won because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;6 SATA bays&lt;/strong&gt; - room to expand storage without external enclosures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4x 2.5G NICs&lt;/strong&gt; - network flexibility for VLANs or link aggregation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intel Quick Sync&lt;/strong&gt; - hardware transcoding for Jellyfin without GPU&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;x86 architecture&lt;/strong&gt; - better Docker compatibility than ARM alternatives&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low power&lt;/strong&gt; - efficient enough to run 24/7&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DDR5 + NVMe&lt;/strong&gt; - modern and fast for running 20+ containers&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Jetson Nano Orin Super
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Role:&lt;/strong&gt; Local voice assistant&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Smart Home Controllers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Philips Hue bridge&lt;/strong&gt; - Zigbee lighting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SwitchBot hub&lt;/strong&gt; - SwitchBot devices integration&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Services &amp;amp; Applications
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Smart Home (Pi 4)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Home Assistant - controlling Hue lights, SwitchBot devices, and automations&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://dev.to/tsukiyo/end-to-end-monitoring-explained-for-homelabs-prometheus-grafana-alertmanager-2g3k"&gt;Monitoring Stack&lt;/a&gt; (Pi 5)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Grafana - dashboards and visualization&lt;/li&gt;
&lt;li&gt;Prometheus - metrics collection&lt;/li&gt;
&lt;li&gt;Alertmanager - alert routing and notifications&lt;/li&gt;
&lt;li&gt;Uptime Kuma - service uptime monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Media Stack (N305)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Jellyfin - media streaming with Intel Quick Sync hardware transcoding&lt;/li&gt;
&lt;li&gt;Komga - comics/manga reader&lt;/li&gt;
&lt;li&gt;Radarr, Sonarr, Prowlarr - media automation (behind Gluetun VPN)&lt;/li&gt;
&lt;li&gt;qBittorrent - downloads (behind VPN)&lt;/li&gt;
&lt;li&gt;Mylar + Kapowarr - comics management&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  File Management (N305)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Samba NAS - network file sharing&lt;/li&gt;
&lt;li&gt;Filebrowser - web UI for downloads&lt;/li&gt;
&lt;li&gt;JDownloader2 - direct downloads&lt;/li&gt;
&lt;li&gt;Syncthing - cross-device file sync&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Applications (N305)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Vaultwarden - password manager (Bitwarden-compatible)&lt;/li&gt;
&lt;li&gt;Immich - photo backup with ML-powered search&lt;/li&gt;
&lt;li&gt;Homer - dashboard&lt;/li&gt;
&lt;li&gt;n8n - workflow automation&lt;/li&gt;
&lt;li&gt;LanguageTool - grammar checker&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Voice Assistant (Jetson)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Local voice control via Home Assistant - no cloud required (&lt;a href="https://tsuki-yo.github.io/PiBlog/posts/voicellm/" rel="noopener noreferrer"&gt;see my previous post&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Network Services (OpenWrt ONE)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Nginx Reverse Proxy - local service names for easy access, no more remembering IP:port combinations&lt;/li&gt;
&lt;li&gt;WireGuard VPN - secure remote access to the homelab when away from home&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Cost Breakdown
&lt;/h2&gt;

&lt;p&gt;Here's roughly what this setup costs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Approx. Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://deskpi.com/products/deskpi-rackmate-t2-rackmount-12u-server-cabinet-for-network-servers-audio-and-video-equipment" rel="noopener noreferrer"&gt;DeskPi RackMate T2 12U&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;~$100-150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.raspberrypi.com/products/raspberry-pi-4-model-b/" rel="noopener noreferrer"&gt;Raspberry Pi 4 4GB&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;~$60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.raspberrypi.com/products/raspberry-pi-5/" rel="noopener noreferrer"&gt;Raspberry Pi 5 8GB&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;~$95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PoE+ HAT x2&lt;/td&gt;
&lt;td&gt;~$50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://cwwk.net/products/12th-i3-n305-n100-nas-motherboard-6-bay-dc-power-2xm-2-nvme-6xsata3-0-pcie-x1-4x-i226-v-2-5g-lan-ddr5-itx-mainboard" rel="noopener noreferrer"&gt;Intel N305 NAS motherboard&lt;/a&gt; (bare board)&lt;/td&gt;
&lt;td&gt;~$200-300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DDR5 RAM (16-32GB)&lt;/td&gt;
&lt;td&gt;~$120-400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVMe SSD (1TB)&lt;/td&gt;
&lt;td&gt;~$70-150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NAS HDD (8TB)&lt;/td&gt;
&lt;td&gt;~$150-200 each&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.nvidia.com/en-us/autonomous-machines/embedded-systems/jetson-orin/nano-super-developer-kit/" rel="noopener noreferrer"&gt;Jetson Orin Nano Super&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;~$249&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.tomshardware.com/networking/open-source-openwrt-one-router-released-at-usd89-hacker-friendly-device-sports-two-ethernet-ports-three-usb-ports-with-dual-band-wi-fi-6" rel="noopener noreferrer"&gt;OpenWrt ONE router&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;~$89&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KeepLink PoE managed switch&lt;/td&gt;
&lt;td&gt;~$100-150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Misc (cables, fans, accessories)&lt;/td&gt;
&lt;td&gt;~$50-100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total (excl. RAM/storage)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$1,000-1,250&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;RAM and storage costs depend on your capacity needs. Due to AI infrastructure demand, DDR5 prices are elevated.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Prices as of January 2026 - check current listings as memory prices are volatile.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Not cheap, but this replaces multiple cloud subscriptions and gives you full control over your data. The N305 and Jetson are the priciest components - you could start smaller with just the Pis and add more later.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Building this setup taught me a few things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Go vertical with an enclosed rack&lt;/strong&gt; - Saves floor space and keeps dust out. The DeskPi 12U rack was a game-changer compared to the open TV rack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use PoE where possible&lt;/strong&gt; - Fewer power adapters, fewer cables, cleaner setup. The managed switch powers both Pis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repurpose what you have&lt;/strong&gt; - The 650W TUF Gaming PSU from my old PC now powers the N305 PC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardware transcoding matters&lt;/strong&gt; - The N305's Intel Quick Sync handles Jellyfin effortlessly. Choose your hardware with your workloads in mind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Take your time&lt;/strong&gt; - Perfecting the setup took a few days. The DeskPi's flexibility with parts and accessories makes it worth the effort.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rack assembly isn't more complex than building a PC - just more of them. If you can do one, you can do this.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;This post covered the architecture - the what and why. In upcoming posts, I'll dive into the how: step-by-step guides for setting up each service, from Jellyfin and the *arr stack to Immich and Vaultwarden.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;write weekly about homelabs, monitoring, and DevOps. If you found this helpful, check out my other posts or subscribe on &lt;a href="https://dev.to/tsukiyo"&gt;Dev.to&lt;/a&gt; for more practical guides like this one.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>docker</category>
      <category>linux</category>
      <category>homelab</category>
    </item>
    <item>
      <title>Automated Budget Local LLM Home Assistant Voice Assistant (No Cloud)</title>
      <dc:creator>Natsuki</dc:creator>
      <pubDate>Tue, 09 Sep 2025 03:00:00 +0000</pubDate>
      <link>https://dev.to/tsukiyo/how-i-built-a-budget-local-home-assistant-voice-assistant-no-nabu-casa-no-cloud-mej</link>
      <guid>https://dev.to/tsukiyo/how-i-built-a-budget-local-home-assistant-voice-assistant-no-nabu-casa-no-cloud-mej</guid>
      <description>&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;If you want Google Assistant voice control in Home Assistant, you usually need a &lt;strong&gt;Nabu Casa subscription&lt;/strong&gt;. But I wanted two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Save money&lt;/strong&gt; — no monthly subscription.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep my privacy&lt;/strong&gt; — no smart-home data leaving my LAN.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Utilize my unused gaming PC&lt;/strong&gt; — put my Intel Arc A580 GPU to work.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So I built my own &lt;strong&gt;fully local voice assistant&lt;/strong&gt; — offline, GPU-accelerated, and integrated with Home Assistant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspiration
&lt;/h2&gt;

&lt;p&gt;This idea was inspired by &lt;a href="https://www.youtube.com/watch?v=XvbVePuP7NY" rel="noopener noreferrer"&gt;NetworkChuck's video&lt;/a&gt;. He showed how to run a Pi satellite with systemd. I extended the idea into a automated setup powered by Docker Desktop and Task Scheduler.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Core Idea
&lt;/h2&gt;

&lt;p&gt;I wanted everything to &lt;strong&gt;start automatically&lt;/strong&gt;, run &lt;strong&gt;fast on my Intel Arc GPU&lt;/strong&gt;, and stay simple. My setup is built on five pillars:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Docker Desktop + Docker Compose&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whisper (STT) + Piper (TTS) defined in Compose.&lt;/li&gt;
&lt;li&gt;Docker Desktop auto-starts on login → containers always online.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Task Scheduler (Windows 11)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Launches Ollama Portable Zip at login.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;IPEX-LLM GPU Acceleration&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lets Ollama run on my &lt;strong&gt;Intel Arc A580&lt;/strong&gt; instead of CPU.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Qwen 3-4B Instruct&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Small but capable LLM for intent recognition.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Instruction Prompt&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tuned Qwen so it only returns actionable device control commands (no fluff).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This combo — &lt;strong&gt;Docker Desktop + Compose + Task Scheduler + IPEX Ollama + Qwen 3-4B&lt;/strong&gt; — gives me a fully automated, private assistant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hardware Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Raspberry Pi Zero 2 W&lt;/strong&gt; with &lt;strong&gt;ReSpeaker 2-Mic HAT v2&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2.5 W mini speaker&lt;/strong&gt; for audio feedback&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows 11 gaming PC&lt;/strong&gt; with &lt;strong&gt;Intel Arc A580 GPU&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Home Assistant&lt;/strong&gt;, self-hosted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Pi works as a &lt;strong&gt;Wyoming Satellite&lt;/strong&gt;. The Windows 11 PC runs Whisper, Piper, and the LLM backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Whisper + Piper (Docker Compose on WSL2)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  docker-compose.yml
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;wyoming-whisper&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rhasspy/wyoming-whisper&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wyoming-whisper&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10300:10300"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;~/whisperdata:/data&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--model"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;small-int8"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--language"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

  &lt;span class="na"&gt;wyoming-piper&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rhasspy/wyoming-piper&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wyoming-piper&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10200:10200"&lt;/span&gt; &lt;span class="c1"&gt;# If container exposes 5000, use 10200:5000&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;~/piperdata:/data&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--voice"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en_US-lessac-medium"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Docker Desktop set to &lt;strong&gt;launch at login&lt;/strong&gt;, these services come online automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Firewall Rules (PowerShell)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;New-NetFirewallRule&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-DisplayName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Wyoming Piper 10200"&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nt"&gt;-Direction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Inbound&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Protocol&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;TCP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LocalPort&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;10200&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Allow&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;New-NetFirewallRule&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-DisplayName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Wyoming Whisper 10300"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Direction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Inbound&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Protocol&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;TCP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LocalPort&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;10300&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Allow&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;New-NetFirewallRule&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-DisplayName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ollama 11436"&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;-Direction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Inbound&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Protocol&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;TCP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LocalPort&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;11436&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Allow&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Ollama Portable Zip with IPEX-LLM (Intel Arc GPUs)
&lt;/h2&gt;

&lt;p&gt;On my Intel Arc A580, I needed GPU acceleration. Since the official Ollama desktop app doesn't support Intel GPUs, I used the &lt;strong&gt;Portable Zip build + IPEX-LLM&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Batch script (run-ollama-gpu.bat)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;@echo &lt;span class="na"&gt;off&lt;/span&gt;
&lt;span class="nb"&gt;setlocal&lt;/span&gt;
&lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="kd"&gt;OLLAMA_ROOT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kd"&gt;C&lt;/span&gt;:\Ollama&lt;span class="na"&gt;-IPEX
&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="kd"&gt;OLLAMA_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0.0.0:11436
&lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="kd"&gt;OLLAMA_NUM_GPU&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;999&lt;/span&gt;
&lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="kd"&gt;OLLAMA_INTEL_GPU&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="kd"&gt;SYCL_DEVICE_FILTER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kd"&gt;level_zero&lt;/span&gt;&lt;span class="nl"&gt;:gpu&lt;/span&gt;

&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="na"&gt;/d &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;%OLLAMA_ROOT%&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;timeout&lt;/span&gt; &lt;span class="na"&gt;/t &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="na"&gt;/nobreak &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="kr"&gt;nul&lt;/span&gt;
&lt;span class="nb"&gt;start&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="na"&gt;/min &lt;/span&gt;&lt;span class="nb"&gt;cmd.exe&lt;/span&gt; &lt;span class="na"&gt;/c &lt;/span&gt;&lt;span class="s2"&gt;"ollama.exe serve &amp;gt;&amp;gt; "&lt;/span&gt;&lt;span class="nv"&gt;%OLLAMA_ROOT%&lt;/span&gt;\ollama.log&lt;span class="s2"&gt;" 2&amp;gt;&amp;amp;1"&lt;/span&gt;
&lt;span class="nb"&gt;endlocal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Task Scheduler
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Trigger: &lt;strong&gt;At logon&lt;/strong&gt; (with ~30s delay).&lt;/li&gt;
&lt;li&gt;Action: runs &lt;code&gt;run-ollama-gpu.bat&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;General tab: &lt;strong&gt;Run with highest privileges&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This ensures &lt;strong&gt;Ollama Portable Zip&lt;/strong&gt; is always running in the background with GPU acceleration.&lt;/p&gt;

&lt;h3&gt;
  
  
  💡 Note for NVIDIA &amp;amp; Apple Users
&lt;/h3&gt;

&lt;p&gt;If you're on &lt;strong&gt;NVIDIA GPU&lt;/strong&gt; or &lt;strong&gt;Apple Silicon&lt;/strong&gt;, you don't need this setup. Just install the official &lt;strong&gt;Ollama Desktop app&lt;/strong&gt;, which supports GPU acceleration and auto-starts automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrating with Home Assistant
&lt;/h2&gt;

&lt;p&gt;To connect all endpoints into Home Assistant, there are &lt;strong&gt;two steps&lt;/strong&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Register Wyoming Protocol Services
&lt;/h3&gt;

&lt;p&gt;Go to &lt;strong&gt;Settings → Devices &amp;amp; Services → Add Integration → Wyoming Protocol&lt;/strong&gt;. Add each endpoint as a service:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Whisper (STT):&lt;/strong&gt; &lt;code&gt;tcp://&amp;lt;PC_IP&amp;gt;:10300&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Piper (TTS):&lt;/strong&gt; &lt;code&gt;tcp://&amp;lt;PC_IP&amp;gt;:10200&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;(Optional) &lt;strong&gt;Pi Satellite:&lt;/strong&gt; &lt;code&gt;tcp://&amp;lt;pi-ip&amp;gt;:10700&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(Home Assistant has OpenWakeWord available as an &lt;strong&gt;Add-on&lt;/strong&gt;, which instantly provides wake word detection support without extra setup.)&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Create a Voice Assistant
&lt;/h3&gt;

&lt;p&gt;Go to &lt;strong&gt;Settings → Voice Assistants&lt;/strong&gt; and create a new assistant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wake Word:&lt;/strong&gt; Select the OpenWakeWord add-on and the wake word.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speech-to-Text (STT):&lt;/strong&gt; Select the Whisper service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text-to-Speech (TTS):&lt;/strong&gt; Select the Piper service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conversation Agent (LLM):&lt;/strong&gt; Select the Ollama service, model: &lt;code&gt;qwen:4b-instruct&lt;/code&gt;.

&lt;ul&gt;
&lt;li&gt;This is where you paste the &lt;strong&gt;instruction prompt&lt;/strong&gt; for Qwen.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Instruction Prompt for Voice Assistant Settings on Home Assistant.
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;Background Information&amp;gt;
You are a smart voice assistant for Home Assistant.
You know everything about my home devices and its states.
Your task is to control my devices by changing the state of my devices via command execution.
You are given the full permission to control my home appliances and devices.

&amp;lt;Instructions&amp;gt;
A user will provide a command to turn a device on or off.
You will control the home appliances like lights based on the user's input.
Below is the example conversation.

&amp;lt;Restrictions&amp;gt;
Don't give me the structured summary of current states.
Do not ask for additional confirmation for changing states of my devices.
Always respond in plain text only, no controlling characters, no emoji.
NEVER use emoji or any non-alphanumeric characters.
Keep the answer simple and to the point.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the wake word works, flowing through: &lt;strong&gt;OpenWakeWord → Whisper → Qwen → Piper → Home Assistant&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;The outcome was &lt;strong&gt;surprisingly good&lt;/strong&gt;. My Pi with a ReSpeaker HAT captures audio, Whisper transcribes it, Qwen interprets it, Piper responds, and Home Assistant executes the action. All &lt;strong&gt;fully offline&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker Desktop + Compose&lt;/strong&gt; → perfect for auto-running Whisper and Piper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task Scheduler + Ollama Portable Zip&lt;/strong&gt; → required for Intel Arc GPUs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NVIDIA / Apple users&lt;/strong&gt; → can just use the Ollama Desktop app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IPEX-LLM on Arc A580&lt;/strong&gt; → makes Qwen 3-4B fast enough for real-time use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom prompt&lt;/strong&gt; → essential to keep responses clean and actionable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Run Whisper and Piper with &lt;strong&gt;GPU acceleration&lt;/strong&gt; on the Intel Arc.&lt;/li&gt;
&lt;li&gt;Try larger models with IPEX-LLM.&lt;/li&gt;
&lt;li&gt;Improve intent handling for complex automations.&lt;/li&gt;
&lt;li&gt;Add &lt;strong&gt;Japanese conversation&lt;/strong&gt; support.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>tutorial</category>
      <category>docker</category>
      <category>homeassistant</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
